CodeMirrorTokenTrackingController.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. /*
  2. * Copyright (C) 2013 Apple Inc. All rights reserved.
  3. *
  4. * Redistribution and use in source and binary forms, with or without
  5. * modification, are permitted provided that the following conditions
  6. * are met:
  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.
  12. *
  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.
  24. */
  25. WebInspector.CodeMirrorTokenTrackingController = function(codeMirror, delegate)
  26. {
  27. WebInspector.Object.call(this);
  28. console.assert(codeMirror);
  29. this._codeMirror = codeMirror;
  30. this._delegate = delegate || null;
  31. this._mode = WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens;
  32. this._mouseOverDelayDuration = 0;
  33. this._mouseOutReleaseDelayDuration = 0;
  34. this._classNameForHighlightedRange = null;
  35. this._tracking = false;
  36. this._hoveredTokenInfo = null;
  37. };
  38. WebInspector.CodeMirrorTokenTrackingController.JumpToSymbolHighlightStyleClassName = "jump-to-symbol-highlight";
  39. WebInspector.CodeMirrorTokenTrackingController.Mode = {
  40. NonSymbolTokens: "non-symbol-tokens",
  41. JavaScriptExpression: "javascript-expression",
  42. }
  43. WebInspector.CodeMirrorTokenTrackingController.prototype = {
  44. constructor: WebInspector.CodeMirrorTokenTrackingController,
  45. // Public
  46. get delegate()
  47. {
  48. return this._delegate;
  49. },
  50. set delegate(x)
  51. {
  52. this._delegate = x;
  53. },
  54. get mode()
  55. {
  56. return this._mode;
  57. },
  58. set mode(x)
  59. {
  60. var oldMode = this._mode;
  61. this._mode = x || WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens;
  62. if (oldMode !== this._mode && this._tracking && this._hoveredTokenInfo)
  63. this._processNewHoveredToken();
  64. },
  65. get mouseOverDelayDuration()
  66. {
  67. return this._mouseOverDelayDuration;
  68. },
  69. set mouseOverDelayDuration(x)
  70. {
  71. console.assert(x >= 0);
  72. this._mouseOverDelayDuration = Math.max(x, 0);
  73. },
  74. get mouseOutReleaseDelayDuration()
  75. {
  76. return this._mouseOutReleaseDelayDuration;
  77. },
  78. set mouseOutReleaseDelayDuration(x)
  79. {
  80. console.assert(x >= 0);
  81. this._mouseOutReleaseDelayDuration = Math.max(x, 0);
  82. },
  83. get classNameForHighlightedRange()
  84. {
  85. return this._classNameForHighlightedRange;
  86. },
  87. set classNameForHighlightedRange(x)
  88. {
  89. this._classNameForHighlightedRange = x || null;
  90. },
  91. get tracking()
  92. {
  93. return this._tracking;
  94. },
  95. get candidate()
  96. {
  97. return this._candidate;
  98. },
  99. startTracking: function()
  100. {
  101. console.assert(!this._tracking);
  102. if (this._tracking)
  103. return;
  104. this._tracking = true;
  105. var wrapper = this._codeMirror.getWrapperElement();
  106. wrapper.addEventListener("mousemove", this, true);
  107. wrapper.addEventListener("mouseout", this, false);
  108. wrapper.addEventListener("mousedown", this, false);
  109. wrapper.addEventListener("mouseup", this, false);
  110. window.addEventListener("blur", this, true);
  111. },
  112. stopTracking: function()
  113. {
  114. console.assert(this._tracking);
  115. if (!this._tracking)
  116. return;
  117. this._tracking = false;
  118. var wrapper = this._codeMirror.getWrapperElement();
  119. wrapper.removeEventListener("mousemove", this, true);
  120. wrapper.removeEventListener("mouseout", this, false);
  121. wrapper.removeEventListener("mousedown", this, false);
  122. wrapper.removeEventListener("mouseup", this, false);
  123. window.removeEventListener("blur", this, true);
  124. window.removeEventListener("mousemove", this, true);
  125. clearTimeout(this._tokenHoverTimer);
  126. delete this._selectionMayBeInProgress;
  127. delete this._hoveredTokenInfo;
  128. },
  129. highlightRange: function(range)
  130. {
  131. this.removeHighlightedRange();
  132. var className = this._classNameForHighlightedRange || "";
  133. this._codeMirrorMarkedText = this._codeMirror.markText(range.start, range.end, {className: className});
  134. window.addEventListener("mousemove", this, true);
  135. },
  136. removeHighlightedRange: function()
  137. {
  138. if (!this._codeMirrorMarkedText)
  139. return;
  140. this._codeMirrorMarkedText.clear();
  141. delete this._codeMirrorMarkedText;
  142. window.removeEventListener("mousemove", this, true);
  143. },
  144. boundsForRange: function(range)
  145. {
  146. var firstCharCoords = this._codeMirror.cursorCoords(range.start);
  147. var lastCharCoords = this._codeMirror.cursorCoords(range.end);
  148. return new WebInspector.Rect(firstCharCoords.left, firstCharCoords.top, lastCharCoords.right - firstCharCoords.left, firstCharCoords.bottom - firstCharCoords.top);
  149. },
  150. // Private
  151. handleEvent: function(event)
  152. {
  153. switch (event.type) {
  154. case "mousemove":
  155. if (event.currentTarget === window)
  156. this._mouseMovedWithMarkedText(event);
  157. else
  158. this._mouseMovedOverEditor(event);
  159. break;
  160. case "mouseout":
  161. // Only deal with a mouseout event that has the editor wrapper as the target.
  162. if (!event.currentTarget.contains(event.relatedTarget))
  163. this._mouseMovedOutOfEditor(event);
  164. break;
  165. case "mousedown":
  166. this._mouseButtonWasPressedOverEditor(event);
  167. break;
  168. case "mouseup":
  169. this._mouseButtonWasReleasedOverEditor(event);
  170. break;
  171. case "blur":
  172. this._windowLostFocus(event);
  173. break;
  174. }
  175. },
  176. _mouseMovedWithMarkedText: function(event)
  177. {
  178. var shouldRelease = !event.target.classList.contains(this._classNameForHighlightedRange);
  179. if (shouldRelease && this._delegate && typeof this._delegate.tokenTrackingControllerCanReleaseHighlightedRange === "function")
  180. shouldRelease = this._delegate.tokenTrackingControllerCanReleaseHighlightedRange(this, event.target);
  181. if (shouldRelease) {
  182. if (!this._markedTextMouseoutTimer)
  183. this._markedTextMouseoutTimer = setTimeout(this._markedTextIsNoLongerHovered.bind(this), this._mouseOutReleaseDelayDuration);
  184. return;
  185. }
  186. clearTimeout(this._markedTextMouseoutTimer);
  187. delete this._markedTextMouseoutTimer;
  188. },
  189. _markedTextIsNoLongerHovered: function()
  190. {
  191. if (this._delegate && typeof this._delegate.tokenTrackingControllerHighlightedRangeReleased === "function")
  192. this._delegate.tokenTrackingControllerHighlightedRangeReleased(this);
  193. delete this._markedTextMouseoutTimer;
  194. },
  195. _mouseMovedOverEditor: function(event)
  196. {
  197. // Get the position in the text and the token at that position.
  198. var position = this._codeMirror.coordsChar({left: event.pageX, top: event.pageY});
  199. var token = this._codeMirror.getTokenAt(position);
  200. if (!token || !token.type || !token.string) {
  201. clearTimeout(this._tokenHoverTimer);
  202. delete this._hoveredTokenInfo;
  203. return;
  204. }
  205. // Stop right here if we're hovering the same token as we were last time.
  206. if (this._hoveredTokenInfo &&
  207. this._hoveredTokenInfo.position.line === position.line &&
  208. this._hoveredTokenInfo.token.start === token.start &&
  209. this._hoveredTokenInfo.token.end === token.end)
  210. return;
  211. // We have a new hovered token.
  212. var innerMode = CodeMirror.innerMode(this._codeMirror.getMode(), token.state);
  213. var codeMirrorModeName = innerMode.mode.alternateName || innerMode.mode.name;
  214. this._hoveredTokenInfo = {
  215. token: token,
  216. position: position,
  217. innerMode: innerMode,
  218. modeName: codeMirrorModeName
  219. };
  220. clearTimeout(this._tokenHoverTimer);
  221. if (this._codeMirrorMarkedText || !this._mouseOverDelayDuration)
  222. this._processNewHoveredToken();
  223. else
  224. this._tokenHoverTimer = setTimeout(this._processNewHoveredToken.bind(this), this._mouseOverDelayDuration);
  225. },
  226. _mouseMovedOutOfEditor: function(event)
  227. {
  228. clearTimeout(this._tokenHoverTimer);
  229. delete this._hoveredTokenInfo;
  230. delete this._selectionMayBeInProgress;
  231. },
  232. _mouseButtonWasPressedOverEditor: function(event)
  233. {
  234. this._selectionMayBeInProgress = true;
  235. },
  236. _mouseButtonWasReleasedOverEditor: function(event)
  237. {
  238. delete this._selectionMayBeInProgress;
  239. this._mouseMovedOverEditor(event);
  240. if (this._codeMirrorMarkedText && this._hoveredTokenInfo) {
  241. var position = this._codeMirror.coordsChar({left: event.pageX, top: event.pageY});
  242. var marks = this._codeMirror.findMarksAt(position);
  243. for (var i = 0; i < marks.length; ++i) {
  244. if (marks[i] === this._codeMirrorMarkedText) {
  245. if (this._delegate && typeof this._delegate.tokenTrackingControllerHighlightedRangeWasClicked === "function") {
  246. // Trigger the clicked delegate asynchronously, letting the editor complete handling of the click.
  247. setTimeout(function() { this._delegate.tokenTrackingControllerHighlightedRangeWasClicked(this); }.bind(this), 0);
  248. }
  249. break;
  250. }
  251. }
  252. }
  253. },
  254. _windowLostFocus: function(event)
  255. {
  256. delete this._selectionMayBeInProgress;
  257. },
  258. _processNewHoveredToken: function()
  259. {
  260. console.assert(this._hoveredTokenInfo);
  261. if (this._selectionMayBeInProgress)
  262. return;
  263. this._candidate = null;
  264. switch (this._mode) {
  265. case WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens:
  266. this._candidate = this._processNonSymbolToken();
  267. break;
  268. case WebInspector.CodeMirrorTokenTrackingController.Mode.JavaScriptExpression:
  269. this._candidate = this._processJavaScriptExpression();
  270. break;
  271. }
  272. if (!this._candidate)
  273. return;
  274. clearTimeout(this._markedTextMouseoutTimer);
  275. delete this._markedTextMouseoutTimer;
  276. if (this._delegate && typeof this._delegate.tokenTrackingControllerNewHighlightCandidate === "function")
  277. this._delegate.tokenTrackingControllerNewHighlightCandidate(this, this._candidate);
  278. },
  279. _processNonSymbolToken: function()
  280. {
  281. // Ignore any symbol tokens.
  282. var type = this._hoveredTokenInfo.token.type;
  283. if (!type)
  284. return null;
  285. var startPosition = {line: this._hoveredTokenInfo.position.line, ch: this._hoveredTokenInfo.token.start};
  286. var endPosition = {line: this._hoveredTokenInfo.position.line, ch: this._hoveredTokenInfo.token.end};
  287. return {
  288. hoveredToken: this._hoveredTokenInfo.token,
  289. hoveredTokenRange: {start: startPosition, end: endPosition},
  290. };
  291. },
  292. _processJavaScriptExpression: function()
  293. {
  294. // Only valid within JavaScript.
  295. if (this._hoveredTokenInfo.modeName !== "javascript")
  296. return null;
  297. // We only handle vars, definitions, properties, and the keyword 'this'.
  298. var type = this._hoveredTokenInfo.token.type;
  299. var isProperty = type.indexOf("property") !== -1;
  300. var isKeyword = type.indexOf("keyword") !== -1;
  301. if (!isProperty && !isKeyword && type.indexOf("variable") === -1 && type.indexOf("def") === -1)
  302. return null;
  303. // Not object literal properties.
  304. var state = this._hoveredTokenInfo.innerMode.state;
  305. if (isProperty && state.lexical && state.lexical.type === "}")
  306. return null;
  307. // Only the "this" keyword.
  308. if (isKeyword && this._hoveredTokenInfo.token.string !== "this")
  309. return null;
  310. // Work out the full hovered expression.
  311. var expression = this._hoveredTokenInfo.token.string;
  312. var expressionStartPosition = {line: this._hoveredTokenInfo.position.line, ch: this._hoveredTokenInfo.token.start};
  313. var startPosition = {line: this._hoveredTokenInfo.position.line, ch: this._hoveredTokenInfo.token.start};
  314. var endPosition = {line: this._hoveredTokenInfo.position.line, ch: this._hoveredTokenInfo.token.end};
  315. while (true) {
  316. var token = this._codeMirror.getTokenAt(expressionStartPosition);
  317. var isDot = token && !token.type && token.string === ".";
  318. var isExpression = token && token.type && token.type.indexOf("m-javascript") !== -1;
  319. if (!isDot && !isExpression)
  320. break;
  321. expression = token.string + expression;
  322. expressionStartPosition.ch = token.start;
  323. }
  324. // Return the candidate for this token and expression.
  325. return {
  326. hoveredToken: this._hoveredTokenInfo.token,
  327. hoveredTokenRange: {start: startPosition, end: endPosition},
  328. expression: expression,
  329. expressionRange: {start: expressionStartPosition, end: endPosition},
  330. };
  331. }
  332. };
  333. WebInspector.CodeMirrorCompletionController.prototype.__proto__ = WebInspector.Object.prototype;