6c8250406fa35adaf08ef59d2c86f788e21ad2a5a8a016b4e94a886a9f0602d80b144285b662e9fa15cd679c258187bb96d0ba114e2e09e042fd9a1ecafbcf 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import { PageCoordinates } from '../editorDom.js';
  6. import { PartFingerprints } from '../view/viewPart.js';
  7. import { ViewLine } from '../viewParts/lines/viewLine.js';
  8. import { Position } from '../../common/core/position.js';
  9. import { Range as EditorRange } from '../../common/core/range.js';
  10. import { CursorColumns } from '../../common/core/cursorColumns.js';
  11. import * as dom from '../../../base/browser/dom.js';
  12. import { AtomicTabMoveOperations } from '../../common/cursor/cursorAtomicMoveOperations.js';
  13. class UnknownHitTestResult {
  14. constructor(hitTarget = null) {
  15. this.hitTarget = hitTarget;
  16. this.type = 0 /* HitTestResultType.Unknown */;
  17. }
  18. }
  19. class ContentHitTestResult {
  20. constructor(position, spanNode, injectedText) {
  21. this.position = position;
  22. this.spanNode = spanNode;
  23. this.injectedText = injectedText;
  24. this.type = 1 /* HitTestResultType.Content */;
  25. }
  26. }
  27. var HitTestResult;
  28. (function (HitTestResult) {
  29. function createFromDOMInfo(ctx, spanNode, offset) {
  30. const position = ctx.getPositionFromDOMInfo(spanNode, offset);
  31. if (position) {
  32. return new ContentHitTestResult(position, spanNode, null);
  33. }
  34. return new UnknownHitTestResult(spanNode);
  35. }
  36. HitTestResult.createFromDOMInfo = createFromDOMInfo;
  37. })(HitTestResult || (HitTestResult = {}));
  38. export class PointerHandlerLastRenderData {
  39. constructor(lastViewCursorsRenderData, lastTextareaPosition) {
  40. this.lastViewCursorsRenderData = lastViewCursorsRenderData;
  41. this.lastTextareaPosition = lastTextareaPosition;
  42. }
  43. }
  44. export class MouseTarget {
  45. static _deduceRage(position, range = null) {
  46. if (!range && position) {
  47. return new EditorRange(position.lineNumber, position.column, position.lineNumber, position.column);
  48. }
  49. return range !== null && range !== void 0 ? range : null;
  50. }
  51. static createUnknown(element, mouseColumn, position) {
  52. return { type: 0 /* MouseTargetType.UNKNOWN */, element, mouseColumn, position, range: this._deduceRage(position) };
  53. }
  54. static createTextarea(element, mouseColumn) {
  55. return { type: 1 /* MouseTargetType.TEXTAREA */, element, mouseColumn, position: null, range: null };
  56. }
  57. static createMargin(type, element, mouseColumn, position, range, detail) {
  58. return { type, element, mouseColumn, position, range, detail };
  59. }
  60. static createViewZone(type, element, mouseColumn, position, detail) {
  61. return { type, element, mouseColumn, position, range: this._deduceRage(position), detail };
  62. }
  63. static createContentText(element, mouseColumn, position, range, detail) {
  64. return { type: 6 /* MouseTargetType.CONTENT_TEXT */, element, mouseColumn, position, range: this._deduceRage(position, range), detail };
  65. }
  66. static createContentEmpty(element, mouseColumn, position, detail) {
  67. return { type: 7 /* MouseTargetType.CONTENT_EMPTY */, element, mouseColumn, position, range: this._deduceRage(position), detail };
  68. }
  69. static createContentWidget(element, mouseColumn, detail) {
  70. return { type: 9 /* MouseTargetType.CONTENT_WIDGET */, element, mouseColumn, position: null, range: null, detail };
  71. }
  72. static createScrollbar(element, mouseColumn, position) {
  73. return { type: 11 /* MouseTargetType.SCROLLBAR */, element, mouseColumn, position, range: this._deduceRage(position) };
  74. }
  75. static createOverlayWidget(element, mouseColumn, detail) {
  76. return { type: 12 /* MouseTargetType.OVERLAY_WIDGET */, element, mouseColumn, position: null, range: null, detail };
  77. }
  78. static createOutsideEditor(mouseColumn, position) {
  79. return { type: 13 /* MouseTargetType.OUTSIDE_EDITOR */, element: null, mouseColumn, position, range: this._deduceRage(position) };
  80. }
  81. static _typeToString(type) {
  82. if (type === 1 /* MouseTargetType.TEXTAREA */) {
  83. return 'TEXTAREA';
  84. }
  85. if (type === 2 /* MouseTargetType.GUTTER_GLYPH_MARGIN */) {
  86. return 'GUTTER_GLYPH_MARGIN';
  87. }
  88. if (type === 3 /* MouseTargetType.GUTTER_LINE_NUMBERS */) {
  89. return 'GUTTER_LINE_NUMBERS';
  90. }
  91. if (type === 4 /* MouseTargetType.GUTTER_LINE_DECORATIONS */) {
  92. return 'GUTTER_LINE_DECORATIONS';
  93. }
  94. if (type === 5 /* MouseTargetType.GUTTER_VIEW_ZONE */) {
  95. return 'GUTTER_VIEW_ZONE';
  96. }
  97. if (type === 6 /* MouseTargetType.CONTENT_TEXT */) {
  98. return 'CONTENT_TEXT';
  99. }
  100. if (type === 7 /* MouseTargetType.CONTENT_EMPTY */) {
  101. return 'CONTENT_EMPTY';
  102. }
  103. if (type === 8 /* MouseTargetType.CONTENT_VIEW_ZONE */) {
  104. return 'CONTENT_VIEW_ZONE';
  105. }
  106. if (type === 9 /* MouseTargetType.CONTENT_WIDGET */) {
  107. return 'CONTENT_WIDGET';
  108. }
  109. if (type === 10 /* MouseTargetType.OVERVIEW_RULER */) {
  110. return 'OVERVIEW_RULER';
  111. }
  112. if (type === 11 /* MouseTargetType.SCROLLBAR */) {
  113. return 'SCROLLBAR';
  114. }
  115. if (type === 12 /* MouseTargetType.OVERLAY_WIDGET */) {
  116. return 'OVERLAY_WIDGET';
  117. }
  118. return 'UNKNOWN';
  119. }
  120. static toString(target) {
  121. return this._typeToString(target.type) + ': ' + target.position + ' - ' + target.range + ' - ' + JSON.stringify(target.detail);
  122. }
  123. }
  124. class ElementPath {
  125. static isTextArea(path) {
  126. return (path.length === 2
  127. && path[0] === 3 /* PartFingerprint.OverflowGuard */
  128. && path[1] === 6 /* PartFingerprint.TextArea */);
  129. }
  130. static isChildOfViewLines(path) {
  131. return (path.length >= 4
  132. && path[0] === 3 /* PartFingerprint.OverflowGuard */
  133. && path[3] === 7 /* PartFingerprint.ViewLines */);
  134. }
  135. static isStrictChildOfViewLines(path) {
  136. return (path.length > 4
  137. && path[0] === 3 /* PartFingerprint.OverflowGuard */
  138. && path[3] === 7 /* PartFingerprint.ViewLines */);
  139. }
  140. static isChildOfScrollableElement(path) {
  141. return (path.length >= 2
  142. && path[0] === 3 /* PartFingerprint.OverflowGuard */
  143. && path[1] === 5 /* PartFingerprint.ScrollableElement */);
  144. }
  145. static isChildOfMinimap(path) {
  146. return (path.length >= 2
  147. && path[0] === 3 /* PartFingerprint.OverflowGuard */
  148. && path[1] === 8 /* PartFingerprint.Minimap */);
  149. }
  150. static isChildOfContentWidgets(path) {
  151. return (path.length >= 4
  152. && path[0] === 3 /* PartFingerprint.OverflowGuard */
  153. && path[3] === 1 /* PartFingerprint.ContentWidgets */);
  154. }
  155. static isChildOfOverflowingContentWidgets(path) {
  156. return (path.length >= 1
  157. && path[0] === 2 /* PartFingerprint.OverflowingContentWidgets */);
  158. }
  159. static isChildOfOverlayWidgets(path) {
  160. return (path.length >= 2
  161. && path[0] === 3 /* PartFingerprint.OverflowGuard */
  162. && path[1] === 4 /* PartFingerprint.OverlayWidgets */);
  163. }
  164. }
  165. export class HitTestContext {
  166. constructor(context, viewHelper, lastRenderData) {
  167. this.viewModel = context.viewModel;
  168. const options = context.configuration.options;
  169. this.layoutInfo = options.get(133 /* EditorOption.layoutInfo */);
  170. this.viewDomNode = viewHelper.viewDomNode;
  171. this.lineHeight = options.get(61 /* EditorOption.lineHeight */);
  172. this.stickyTabStops = options.get(106 /* EditorOption.stickyTabStops */);
  173. this.typicalHalfwidthCharacterWidth = options.get(46 /* EditorOption.fontInfo */).typicalHalfwidthCharacterWidth;
  174. this.lastRenderData = lastRenderData;
  175. this._context = context;
  176. this._viewHelper = viewHelper;
  177. }
  178. getZoneAtCoord(mouseVerticalOffset) {
  179. return HitTestContext.getZoneAtCoord(this._context, mouseVerticalOffset);
  180. }
  181. static getZoneAtCoord(context, mouseVerticalOffset) {
  182. // The target is either a view zone or the empty space after the last view-line
  183. const viewZoneWhitespace = context.viewLayout.getWhitespaceAtVerticalOffset(mouseVerticalOffset);
  184. if (viewZoneWhitespace) {
  185. const viewZoneMiddle = viewZoneWhitespace.verticalOffset + viewZoneWhitespace.height / 2;
  186. const lineCount = context.viewModel.getLineCount();
  187. let positionBefore = null;
  188. let position;
  189. let positionAfter = null;
  190. if (viewZoneWhitespace.afterLineNumber !== lineCount) {
  191. // There are more lines after this view zone
  192. positionAfter = new Position(viewZoneWhitespace.afterLineNumber + 1, 1);
  193. }
  194. if (viewZoneWhitespace.afterLineNumber > 0) {
  195. // There are more lines above this view zone
  196. positionBefore = new Position(viewZoneWhitespace.afterLineNumber, context.viewModel.getLineMaxColumn(viewZoneWhitespace.afterLineNumber));
  197. }
  198. if (positionAfter === null) {
  199. position = positionBefore;
  200. }
  201. else if (positionBefore === null) {
  202. position = positionAfter;
  203. }
  204. else if (mouseVerticalOffset < viewZoneMiddle) {
  205. position = positionBefore;
  206. }
  207. else {
  208. position = positionAfter;
  209. }
  210. return {
  211. viewZoneId: viewZoneWhitespace.id,
  212. afterLineNumber: viewZoneWhitespace.afterLineNumber,
  213. positionBefore: positionBefore,
  214. positionAfter: positionAfter,
  215. position: position
  216. };
  217. }
  218. return null;
  219. }
  220. getFullLineRangeAtCoord(mouseVerticalOffset) {
  221. if (this._context.viewLayout.isAfterLines(mouseVerticalOffset)) {
  222. // Below the last line
  223. const lineNumber = this._context.viewModel.getLineCount();
  224. const maxLineColumn = this._context.viewModel.getLineMaxColumn(lineNumber);
  225. return {
  226. range: new EditorRange(lineNumber, maxLineColumn, lineNumber, maxLineColumn),
  227. isAfterLines: true
  228. };
  229. }
  230. const lineNumber = this._context.viewLayout.getLineNumberAtVerticalOffset(mouseVerticalOffset);
  231. const maxLineColumn = this._context.viewModel.getLineMaxColumn(lineNumber);
  232. return {
  233. range: new EditorRange(lineNumber, 1, lineNumber, maxLineColumn),
  234. isAfterLines: false
  235. };
  236. }
  237. getLineNumberAtVerticalOffset(mouseVerticalOffset) {
  238. return this._context.viewLayout.getLineNumberAtVerticalOffset(mouseVerticalOffset);
  239. }
  240. isAfterLines(mouseVerticalOffset) {
  241. return this._context.viewLayout.isAfterLines(mouseVerticalOffset);
  242. }
  243. isInTopPadding(mouseVerticalOffset) {
  244. return this._context.viewLayout.isInTopPadding(mouseVerticalOffset);
  245. }
  246. isInBottomPadding(mouseVerticalOffset) {
  247. return this._context.viewLayout.isInBottomPadding(mouseVerticalOffset);
  248. }
  249. getVerticalOffsetForLineNumber(lineNumber) {
  250. return this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber);
  251. }
  252. findAttribute(element, attr) {
  253. return HitTestContext._findAttribute(element, attr, this._viewHelper.viewDomNode);
  254. }
  255. static _findAttribute(element, attr, stopAt) {
  256. while (element && element !== document.body) {
  257. if (element.hasAttribute && element.hasAttribute(attr)) {
  258. return element.getAttribute(attr);
  259. }
  260. if (element === stopAt) {
  261. return null;
  262. }
  263. element = element.parentNode;
  264. }
  265. return null;
  266. }
  267. getLineWidth(lineNumber) {
  268. return this._viewHelper.getLineWidth(lineNumber);
  269. }
  270. visibleRangeForPosition(lineNumber, column) {
  271. return this._viewHelper.visibleRangeForPosition(lineNumber, column);
  272. }
  273. getPositionFromDOMInfo(spanNode, offset) {
  274. return this._viewHelper.getPositionFromDOMInfo(spanNode, offset);
  275. }
  276. getCurrentScrollTop() {
  277. return this._context.viewLayout.getCurrentScrollTop();
  278. }
  279. getCurrentScrollLeft() {
  280. return this._context.viewLayout.getCurrentScrollLeft();
  281. }
  282. }
  283. class BareHitTestRequest {
  284. constructor(ctx, editorPos, pos, relativePos) {
  285. this.editorPos = editorPos;
  286. this.pos = pos;
  287. this.relativePos = relativePos;
  288. this.mouseVerticalOffset = Math.max(0, ctx.getCurrentScrollTop() + this.relativePos.y);
  289. this.mouseContentHorizontalOffset = ctx.getCurrentScrollLeft() + this.relativePos.x - ctx.layoutInfo.contentLeft;
  290. this.isInMarginArea = (this.relativePos.x < ctx.layoutInfo.contentLeft && this.relativePos.x >= ctx.layoutInfo.glyphMarginLeft);
  291. this.isInContentArea = !this.isInMarginArea;
  292. this.mouseColumn = Math.max(0, MouseTargetFactory._getMouseColumn(this.mouseContentHorizontalOffset, ctx.typicalHalfwidthCharacterWidth));
  293. }
  294. }
  295. class HitTestRequest extends BareHitTestRequest {
  296. constructor(ctx, editorPos, pos, relativePos, target) {
  297. super(ctx, editorPos, pos, relativePos);
  298. this._ctx = ctx;
  299. if (target) {
  300. this.target = target;
  301. this.targetPath = PartFingerprints.collect(target, ctx.viewDomNode);
  302. }
  303. else {
  304. this.target = null;
  305. this.targetPath = new Uint8Array(0);
  306. }
  307. }
  308. toString() {
  309. return `pos(${this.pos.x},${this.pos.y}), editorPos(${this.editorPos.x},${this.editorPos.y}), relativePos(${this.relativePos.x},${this.relativePos.y}), mouseVerticalOffset: ${this.mouseVerticalOffset}, mouseContentHorizontalOffset: ${this.mouseContentHorizontalOffset}\n\ttarget: ${this.target ? this.target.outerHTML : null}`;
  310. }
  311. _getMouseColumn(position = null) {
  312. if (position && position.column < this._ctx.viewModel.getLineMaxColumn(position.lineNumber)) {
  313. // Most likely, the line contains foreign decorations...
  314. return CursorColumns.visibleColumnFromColumn(this._ctx.viewModel.getLineContent(position.lineNumber), position.column, this._ctx.viewModel.model.getOptions().tabSize) + 1;
  315. }
  316. return this.mouseColumn;
  317. }
  318. fulfillUnknown(position = null) {
  319. return MouseTarget.createUnknown(this.target, this._getMouseColumn(position), position);
  320. }
  321. fulfillTextarea() {
  322. return MouseTarget.createTextarea(this.target, this._getMouseColumn());
  323. }
  324. fulfillMargin(type, position, range, detail) {
  325. return MouseTarget.createMargin(type, this.target, this._getMouseColumn(position), position, range, detail);
  326. }
  327. fulfillViewZone(type, position, detail) {
  328. return MouseTarget.createViewZone(type, this.target, this._getMouseColumn(position), position, detail);
  329. }
  330. fulfillContentText(position, range, detail) {
  331. return MouseTarget.createContentText(this.target, this._getMouseColumn(position), position, range, detail);
  332. }
  333. fulfillContentEmpty(position, detail) {
  334. return MouseTarget.createContentEmpty(this.target, this._getMouseColumn(position), position, detail);
  335. }
  336. fulfillContentWidget(detail) {
  337. return MouseTarget.createContentWidget(this.target, this._getMouseColumn(), detail);
  338. }
  339. fulfillScrollbar(position) {
  340. return MouseTarget.createScrollbar(this.target, this._getMouseColumn(position), position);
  341. }
  342. fulfillOverlayWidget(detail) {
  343. return MouseTarget.createOverlayWidget(this.target, this._getMouseColumn(), detail);
  344. }
  345. withTarget(target) {
  346. return new HitTestRequest(this._ctx, this.editorPos, this.pos, this.relativePos, target);
  347. }
  348. }
  349. const EMPTY_CONTENT_AFTER_LINES = { isAfterLines: true };
  350. function createEmptyContentDataInLines(horizontalDistanceToText) {
  351. return {
  352. isAfterLines: false,
  353. horizontalDistanceToText: horizontalDistanceToText
  354. };
  355. }
  356. export class MouseTargetFactory {
  357. constructor(context, viewHelper) {
  358. this._context = context;
  359. this._viewHelper = viewHelper;
  360. }
  361. mouseTargetIsWidget(e) {
  362. const t = e.target;
  363. const path = PartFingerprints.collect(t, this._viewHelper.viewDomNode);
  364. // Is it a content widget?
  365. if (ElementPath.isChildOfContentWidgets(path) || ElementPath.isChildOfOverflowingContentWidgets(path)) {
  366. return true;
  367. }
  368. // Is it an overlay widget?
  369. if (ElementPath.isChildOfOverlayWidgets(path)) {
  370. return true;
  371. }
  372. return false;
  373. }
  374. createMouseTarget(lastRenderData, editorPos, pos, relativePos, target) {
  375. const ctx = new HitTestContext(this._context, this._viewHelper, lastRenderData);
  376. const request = new HitTestRequest(ctx, editorPos, pos, relativePos, target);
  377. try {
  378. const r = MouseTargetFactory._createMouseTarget(ctx, request, false);
  379. // console.log(MouseTarget.toString(r));
  380. return r;
  381. }
  382. catch (err) {
  383. // console.log(err);
  384. return request.fulfillUnknown();
  385. }
  386. }
  387. static _createMouseTarget(ctx, request, domHitTestExecuted) {
  388. // console.log(`${domHitTestExecuted ? '=>' : ''}CAME IN REQUEST: ${request}`);
  389. // First ensure the request has a target
  390. if (request.target === null) {
  391. if (domHitTestExecuted) {
  392. // Still no target... and we have already executed hit test...
  393. return request.fulfillUnknown();
  394. }
  395. const hitTestResult = MouseTargetFactory._doHitTest(ctx, request);
  396. if (hitTestResult.type === 1 /* HitTestResultType.Content */) {
  397. return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position, hitTestResult.injectedText);
  398. }
  399. return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true);
  400. }
  401. // we know for a fact that request.target is not null
  402. const resolvedRequest = request;
  403. let result = null;
  404. result = result || MouseTargetFactory._hitTestContentWidget(ctx, resolvedRequest);
  405. result = result || MouseTargetFactory._hitTestOverlayWidget(ctx, resolvedRequest);
  406. result = result || MouseTargetFactory._hitTestMinimap(ctx, resolvedRequest);
  407. result = result || MouseTargetFactory._hitTestScrollbarSlider(ctx, resolvedRequest);
  408. result = result || MouseTargetFactory._hitTestViewZone(ctx, resolvedRequest);
  409. result = result || MouseTargetFactory._hitTestMargin(ctx, resolvedRequest);
  410. result = result || MouseTargetFactory._hitTestViewCursor(ctx, resolvedRequest);
  411. result = result || MouseTargetFactory._hitTestTextArea(ctx, resolvedRequest);
  412. result = result || MouseTargetFactory._hitTestViewLines(ctx, resolvedRequest, domHitTestExecuted);
  413. result = result || MouseTargetFactory._hitTestScrollbar(ctx, resolvedRequest);
  414. return (result || request.fulfillUnknown());
  415. }
  416. static _hitTestContentWidget(ctx, request) {
  417. // Is it a content widget?
  418. if (ElementPath.isChildOfContentWidgets(request.targetPath) || ElementPath.isChildOfOverflowingContentWidgets(request.targetPath)) {
  419. const widgetId = ctx.findAttribute(request.target, 'widgetId');
  420. if (widgetId) {
  421. return request.fulfillContentWidget(widgetId);
  422. }
  423. else {
  424. return request.fulfillUnknown();
  425. }
  426. }
  427. return null;
  428. }
  429. static _hitTestOverlayWidget(ctx, request) {
  430. // Is it an overlay widget?
  431. if (ElementPath.isChildOfOverlayWidgets(request.targetPath)) {
  432. const widgetId = ctx.findAttribute(request.target, 'widgetId');
  433. if (widgetId) {
  434. return request.fulfillOverlayWidget(widgetId);
  435. }
  436. else {
  437. return request.fulfillUnknown();
  438. }
  439. }
  440. return null;
  441. }
  442. static _hitTestViewCursor(ctx, request) {
  443. if (request.target) {
  444. // Check if we've hit a painted cursor
  445. const lastViewCursorsRenderData = ctx.lastRenderData.lastViewCursorsRenderData;
  446. for (const d of lastViewCursorsRenderData) {
  447. if (request.target === d.domNode) {
  448. return request.fulfillContentText(d.position, null, { mightBeForeignElement: false, injectedText: null });
  449. }
  450. }
  451. }
  452. if (request.isInContentArea) {
  453. // Edge has a bug when hit-testing the exact position of a cursor,
  454. // instead of returning the correct dom node, it returns the
  455. // first or last rendered view line dom node, therefore help it out
  456. // and first check if we are on top of a cursor
  457. const lastViewCursorsRenderData = ctx.lastRenderData.lastViewCursorsRenderData;
  458. const mouseContentHorizontalOffset = request.mouseContentHorizontalOffset;
  459. const mouseVerticalOffset = request.mouseVerticalOffset;
  460. for (const d of lastViewCursorsRenderData) {
  461. if (mouseContentHorizontalOffset < d.contentLeft) {
  462. // mouse position is to the left of the cursor
  463. continue;
  464. }
  465. if (mouseContentHorizontalOffset > d.contentLeft + d.width) {
  466. // mouse position is to the right of the cursor
  467. continue;
  468. }
  469. const cursorVerticalOffset = ctx.getVerticalOffsetForLineNumber(d.position.lineNumber);
  470. if (cursorVerticalOffset <= mouseVerticalOffset
  471. && mouseVerticalOffset <= cursorVerticalOffset + d.height) {
  472. return request.fulfillContentText(d.position, null, { mightBeForeignElement: false, injectedText: null });
  473. }
  474. }
  475. }
  476. return null;
  477. }
  478. static _hitTestViewZone(ctx, request) {
  479. const viewZoneData = ctx.getZoneAtCoord(request.mouseVerticalOffset);
  480. if (viewZoneData) {
  481. const mouseTargetType = (request.isInContentArea ? 8 /* MouseTargetType.CONTENT_VIEW_ZONE */ : 5 /* MouseTargetType.GUTTER_VIEW_ZONE */);
  482. return request.fulfillViewZone(mouseTargetType, viewZoneData.position, viewZoneData);
  483. }
  484. return null;
  485. }
  486. static _hitTestTextArea(ctx, request) {
  487. // Is it the textarea?
  488. if (ElementPath.isTextArea(request.targetPath)) {
  489. if (ctx.lastRenderData.lastTextareaPosition) {
  490. return request.fulfillContentText(ctx.lastRenderData.lastTextareaPosition, null, { mightBeForeignElement: false, injectedText: null });
  491. }
  492. return request.fulfillTextarea();
  493. }
  494. return null;
  495. }
  496. static _hitTestMargin(ctx, request) {
  497. if (request.isInMarginArea) {
  498. const res = ctx.getFullLineRangeAtCoord(request.mouseVerticalOffset);
  499. const pos = res.range.getStartPosition();
  500. let offset = Math.abs(request.relativePos.x);
  501. const detail = {
  502. isAfterLines: res.isAfterLines,
  503. glyphMarginLeft: ctx.layoutInfo.glyphMarginLeft,
  504. glyphMarginWidth: ctx.layoutInfo.glyphMarginWidth,
  505. lineNumbersWidth: ctx.layoutInfo.lineNumbersWidth,
  506. offsetX: offset
  507. };
  508. offset -= ctx.layoutInfo.glyphMarginLeft;
  509. if (offset <= ctx.layoutInfo.glyphMarginWidth) {
  510. // On the glyph margin
  511. return request.fulfillMargin(2 /* MouseTargetType.GUTTER_GLYPH_MARGIN */, pos, res.range, detail);
  512. }
  513. offset -= ctx.layoutInfo.glyphMarginWidth;
  514. if (offset <= ctx.layoutInfo.lineNumbersWidth) {
  515. // On the line numbers
  516. return request.fulfillMargin(3 /* MouseTargetType.GUTTER_LINE_NUMBERS */, pos, res.range, detail);
  517. }
  518. offset -= ctx.layoutInfo.lineNumbersWidth;
  519. // On the line decorations
  520. return request.fulfillMargin(4 /* MouseTargetType.GUTTER_LINE_DECORATIONS */, pos, res.range, detail);
  521. }
  522. return null;
  523. }
  524. static _hitTestViewLines(ctx, request, domHitTestExecuted) {
  525. if (!ElementPath.isChildOfViewLines(request.targetPath)) {
  526. return null;
  527. }
  528. if (ctx.isInTopPadding(request.mouseVerticalOffset)) {
  529. return request.fulfillContentEmpty(new Position(1, 1), EMPTY_CONTENT_AFTER_LINES);
  530. }
  531. // Check if it is below any lines and any view zones
  532. if (ctx.isAfterLines(request.mouseVerticalOffset) || ctx.isInBottomPadding(request.mouseVerticalOffset)) {
  533. // This most likely indicates it happened after the last view-line
  534. const lineCount = ctx.viewModel.getLineCount();
  535. const maxLineColumn = ctx.viewModel.getLineMaxColumn(lineCount);
  536. return request.fulfillContentEmpty(new Position(lineCount, maxLineColumn), EMPTY_CONTENT_AFTER_LINES);
  537. }
  538. if (domHitTestExecuted) {
  539. // Check if we are hitting a view-line (can happen in the case of inline decorations on empty lines)
  540. // See https://github.com/microsoft/vscode/issues/46942
  541. if (ElementPath.isStrictChildOfViewLines(request.targetPath)) {
  542. const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
  543. if (ctx.viewModel.getLineLength(lineNumber) === 0) {
  544. const lineWidth = ctx.getLineWidth(lineNumber);
  545. const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
  546. return request.fulfillContentEmpty(new Position(lineNumber, 1), detail);
  547. }
  548. const lineWidth = ctx.getLineWidth(lineNumber);
  549. if (request.mouseContentHorizontalOffset >= lineWidth) {
  550. const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
  551. const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
  552. return request.fulfillContentEmpty(pos, detail);
  553. }
  554. }
  555. // We have already executed hit test...
  556. return request.fulfillUnknown();
  557. }
  558. const hitTestResult = MouseTargetFactory._doHitTest(ctx, request);
  559. if (hitTestResult.type === 1 /* HitTestResultType.Content */) {
  560. return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position, hitTestResult.injectedText);
  561. }
  562. return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true);
  563. }
  564. static _hitTestMinimap(ctx, request) {
  565. if (ElementPath.isChildOfMinimap(request.targetPath)) {
  566. const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
  567. const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);
  568. return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));
  569. }
  570. return null;
  571. }
  572. static _hitTestScrollbarSlider(ctx, request) {
  573. if (ElementPath.isChildOfScrollableElement(request.targetPath)) {
  574. if (request.target && request.target.nodeType === 1) {
  575. const className = request.target.className;
  576. if (className && /\b(slider|scrollbar)\b/.test(className)) {
  577. const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
  578. const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);
  579. return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));
  580. }
  581. }
  582. }
  583. return null;
  584. }
  585. static _hitTestScrollbar(ctx, request) {
  586. // Is it the overview ruler?
  587. // Is it a child of the scrollable element?
  588. if (ElementPath.isChildOfScrollableElement(request.targetPath)) {
  589. const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
  590. const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);
  591. return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));
  592. }
  593. return null;
  594. }
  595. getMouseColumn(relativePos) {
  596. const options = this._context.configuration.options;
  597. const layoutInfo = options.get(133 /* EditorOption.layoutInfo */);
  598. const mouseContentHorizontalOffset = this._context.viewLayout.getCurrentScrollLeft() + relativePos.x - layoutInfo.contentLeft;
  599. return MouseTargetFactory._getMouseColumn(mouseContentHorizontalOffset, options.get(46 /* EditorOption.fontInfo */).typicalHalfwidthCharacterWidth);
  600. }
  601. static _getMouseColumn(mouseContentHorizontalOffset, typicalHalfwidthCharacterWidth) {
  602. if (mouseContentHorizontalOffset < 0) {
  603. return 1;
  604. }
  605. const chars = Math.round(mouseContentHorizontalOffset / typicalHalfwidthCharacterWidth);
  606. return (chars + 1);
  607. }
  608. static createMouseTargetFromHitTestPosition(ctx, request, spanNode, pos, injectedText) {
  609. const lineNumber = pos.lineNumber;
  610. const column = pos.column;
  611. const lineWidth = ctx.getLineWidth(lineNumber);
  612. if (request.mouseContentHorizontalOffset > lineWidth) {
  613. const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
  614. return request.fulfillContentEmpty(pos, detail);
  615. }
  616. const visibleRange = ctx.visibleRangeForPosition(lineNumber, column);
  617. if (!visibleRange) {
  618. return request.fulfillUnknown(pos);
  619. }
  620. const columnHorizontalOffset = visibleRange.left;
  621. if (request.mouseContentHorizontalOffset === columnHorizontalOffset) {
  622. return request.fulfillContentText(pos, null, { mightBeForeignElement: !!injectedText, injectedText });
  623. }
  624. const points = [];
  625. points.push({ offset: visibleRange.left, column: column });
  626. if (column > 1) {
  627. const visibleRange = ctx.visibleRangeForPosition(lineNumber, column - 1);
  628. if (visibleRange) {
  629. points.push({ offset: visibleRange.left, column: column - 1 });
  630. }
  631. }
  632. const lineMaxColumn = ctx.viewModel.getLineMaxColumn(lineNumber);
  633. if (column < lineMaxColumn) {
  634. const visibleRange = ctx.visibleRangeForPosition(lineNumber, column + 1);
  635. if (visibleRange) {
  636. points.push({ offset: visibleRange.left, column: column + 1 });
  637. }
  638. }
  639. points.sort((a, b) => a.offset - b.offset);
  640. const mouseCoordinates = request.pos.toClientCoordinates();
  641. const spanNodeClientRect = spanNode.getBoundingClientRect();
  642. const mouseIsOverSpanNode = (spanNodeClientRect.left <= mouseCoordinates.clientX && mouseCoordinates.clientX <= spanNodeClientRect.right);
  643. for (let i = 1; i < points.length; i++) {
  644. const prev = points[i - 1];
  645. const curr = points[i];
  646. if (prev.offset <= request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset <= curr.offset) {
  647. const rng = new EditorRange(lineNumber, prev.column, lineNumber, curr.column);
  648. // See https://github.com/microsoft/vscode/issues/152819
  649. // Due to the use of zwj, the browser's hit test result is skewed towards the left
  650. // Here we try to correct that if the mouse horizontal offset is closer to the right than the left
  651. const prevDelta = Math.abs(prev.offset - request.mouseContentHorizontalOffset);
  652. const nextDelta = Math.abs(curr.offset - request.mouseContentHorizontalOffset);
  653. const resultPos = (prevDelta < nextDelta
  654. ? new Position(lineNumber, prev.column)
  655. : new Position(lineNumber, curr.column));
  656. return request.fulfillContentText(resultPos, rng, { mightBeForeignElement: !mouseIsOverSpanNode || !!injectedText, injectedText });
  657. }
  658. }
  659. return request.fulfillContentText(pos, null, { mightBeForeignElement: !mouseIsOverSpanNode || !!injectedText, injectedText });
  660. }
  661. /**
  662. * Most probably WebKit browsers and Edge
  663. */
  664. static _doHitTestWithCaretRangeFromPoint(ctx, request) {
  665. // In Chrome, especially on Linux it is possible to click between lines,
  666. // so try to adjust the `hity` below so that it lands in the center of a line
  667. const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
  668. const lineVerticalOffset = ctx.getVerticalOffsetForLineNumber(lineNumber);
  669. const lineCenteredVerticalOffset = lineVerticalOffset + Math.floor(ctx.lineHeight / 2);
  670. let adjustedPageY = request.pos.y + (lineCenteredVerticalOffset - request.mouseVerticalOffset);
  671. if (adjustedPageY <= request.editorPos.y) {
  672. adjustedPageY = request.editorPos.y + 1;
  673. }
  674. if (adjustedPageY >= request.editorPos.y + request.editorPos.height) {
  675. adjustedPageY = request.editorPos.y + request.editorPos.height - 1;
  676. }
  677. const adjustedPage = new PageCoordinates(request.pos.x, adjustedPageY);
  678. const r = this._actualDoHitTestWithCaretRangeFromPoint(ctx, adjustedPage.toClientCoordinates());
  679. if (r.type === 1 /* HitTestResultType.Content */) {
  680. return r;
  681. }
  682. // Also try to hit test without the adjustment (for the edge cases that we are near the top or bottom)
  683. return this._actualDoHitTestWithCaretRangeFromPoint(ctx, request.pos.toClientCoordinates());
  684. }
  685. static _actualDoHitTestWithCaretRangeFromPoint(ctx, coords) {
  686. const shadowRoot = dom.getShadowRoot(ctx.viewDomNode);
  687. let range;
  688. if (shadowRoot) {
  689. if (typeof shadowRoot.caretRangeFromPoint === 'undefined') {
  690. range = shadowCaretRangeFromPoint(shadowRoot, coords.clientX, coords.clientY);
  691. }
  692. else {
  693. range = shadowRoot.caretRangeFromPoint(coords.clientX, coords.clientY);
  694. }
  695. }
  696. else {
  697. range = document.caretRangeFromPoint(coords.clientX, coords.clientY);
  698. }
  699. if (!range || !range.startContainer) {
  700. return new UnknownHitTestResult();
  701. }
  702. // Chrome always hits a TEXT_NODE, while Edge sometimes hits a token span
  703. const startContainer = range.startContainer;
  704. if (startContainer.nodeType === startContainer.TEXT_NODE) {
  705. // startContainer is expected to be the token text
  706. const parent1 = startContainer.parentNode; // expected to be the token span
  707. const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span
  708. const parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div
  709. const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? parent3.className : null;
  710. if (parent3ClassName === ViewLine.CLASS_NAME) {
  711. return HitTestResult.createFromDOMInfo(ctx, parent1, range.startOffset);
  712. }
  713. else {
  714. return new UnknownHitTestResult(startContainer.parentNode);
  715. }
  716. }
  717. else if (startContainer.nodeType === startContainer.ELEMENT_NODE) {
  718. // startContainer is expected to be the token span
  719. const parent1 = startContainer.parentNode; // expected to be the view line container span
  720. const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line div
  721. const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? parent2.className : null;
  722. if (parent2ClassName === ViewLine.CLASS_NAME) {
  723. return HitTestResult.createFromDOMInfo(ctx, startContainer, startContainer.textContent.length);
  724. }
  725. else {
  726. return new UnknownHitTestResult(startContainer);
  727. }
  728. }
  729. return new UnknownHitTestResult();
  730. }
  731. /**
  732. * Most probably Gecko
  733. */
  734. static _doHitTestWithCaretPositionFromPoint(ctx, coords) {
  735. const hitResult = document.caretPositionFromPoint(coords.clientX, coords.clientY);
  736. if (hitResult.offsetNode.nodeType === hitResult.offsetNode.TEXT_NODE) {
  737. // offsetNode is expected to be the token text
  738. const parent1 = hitResult.offsetNode.parentNode; // expected to be the token span
  739. const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span
  740. const parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div
  741. const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? parent3.className : null;
  742. if (parent3ClassName === ViewLine.CLASS_NAME) {
  743. return HitTestResult.createFromDOMInfo(ctx, hitResult.offsetNode.parentNode, hitResult.offset);
  744. }
  745. else {
  746. return new UnknownHitTestResult(hitResult.offsetNode.parentNode);
  747. }
  748. }
  749. // For inline decorations, Gecko sometimes returns the `<span>` of the line and the offset is the `<span>` with the inline decoration
  750. // Some other times, it returns the `<span>` with the inline decoration
  751. if (hitResult.offsetNode.nodeType === hitResult.offsetNode.ELEMENT_NODE) {
  752. const parent1 = hitResult.offsetNode.parentNode;
  753. const parent1ClassName = parent1 && parent1.nodeType === parent1.ELEMENT_NODE ? parent1.className : null;
  754. const parent2 = parent1 ? parent1.parentNode : null;
  755. const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? parent2.className : null;
  756. if (parent1ClassName === ViewLine.CLASS_NAME) {
  757. // it returned the `<span>` of the line and the offset is the `<span>` with the inline decoration
  758. const tokenSpan = hitResult.offsetNode.childNodes[Math.min(hitResult.offset, hitResult.offsetNode.childNodes.length - 1)];
  759. if (tokenSpan) {
  760. return HitTestResult.createFromDOMInfo(ctx, tokenSpan, 0);
  761. }
  762. }
  763. else if (parent2ClassName === ViewLine.CLASS_NAME) {
  764. // it returned the `<span>` with the inline decoration
  765. return HitTestResult.createFromDOMInfo(ctx, hitResult.offsetNode, 0);
  766. }
  767. }
  768. return new UnknownHitTestResult(hitResult.offsetNode);
  769. }
  770. static _snapToSoftTabBoundary(position, viewModel) {
  771. const lineContent = viewModel.getLineContent(position.lineNumber);
  772. const { tabSize } = viewModel.model.getOptions();
  773. const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - 1, tabSize, 2 /* Direction.Nearest */);
  774. if (newPosition !== -1) {
  775. return new Position(position.lineNumber, newPosition + 1);
  776. }
  777. return position;
  778. }
  779. static _doHitTest(ctx, request) {
  780. let result = new UnknownHitTestResult();
  781. if (typeof document.caretRangeFromPoint === 'function') {
  782. result = this._doHitTestWithCaretRangeFromPoint(ctx, request);
  783. }
  784. else if (document.caretPositionFromPoint) {
  785. result = this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates());
  786. }
  787. if (result.type === 1 /* HitTestResultType.Content */) {
  788. const injectedText = ctx.viewModel.getInjectedTextAt(result.position);
  789. const normalizedPosition = ctx.viewModel.normalizePosition(result.position, 2 /* PositionAffinity.None */);
  790. if (injectedText || !normalizedPosition.equals(result.position)) {
  791. result = new ContentHitTestResult(normalizedPosition, result.spanNode, injectedText);
  792. }
  793. }
  794. // Snap to the nearest soft tab boundary if atomic soft tabs are enabled.
  795. if (result.type === 1 /* HitTestResultType.Content */ && ctx.stickyTabStops) {
  796. result = new ContentHitTestResult(this._snapToSoftTabBoundary(result.position, ctx.viewModel), result.spanNode, result.injectedText);
  797. }
  798. return result;
  799. }
  800. }
  801. export function shadowCaretRangeFromPoint(shadowRoot, x, y) {
  802. const range = document.createRange();
  803. // Get the element under the point
  804. let el = shadowRoot.elementFromPoint(x, y);
  805. if (el !== null) {
  806. // Get the last child of the element until its firstChild is a text node
  807. // This assumes that the pointer is on the right of the line, out of the tokens
  808. // and that we want to get the offset of the last token of the line
  809. while (el && el.firstChild && el.firstChild.nodeType !== el.firstChild.TEXT_NODE && el.lastChild && el.lastChild.firstChild) {
  810. el = el.lastChild;
  811. }
  812. // Grab its rect
  813. const rect = el.getBoundingClientRect();
  814. // And its font
  815. const font = window.getComputedStyle(el, null).getPropertyValue('font');
  816. // And also its txt content
  817. const text = el.innerText;
  818. // Position the pixel cursor at the left of the element
  819. let pixelCursor = rect.left;
  820. let offset = 0;
  821. let step;
  822. // If the point is on the right of the box put the cursor after the last character
  823. if (x > rect.left + rect.width) {
  824. offset = text.length;
  825. }
  826. else {
  827. const charWidthReader = CharWidthReader.getInstance();
  828. // Goes through all the characters of the innerText, and checks if the x of the point
  829. // belongs to the character.
  830. for (let i = 0; i < text.length + 1; i++) {
  831. // The step is half the width of the character
  832. step = charWidthReader.getCharWidth(text.charAt(i), font) / 2;
  833. // Move to the center of the character
  834. pixelCursor += step;
  835. // If the x of the point is smaller that the position of the cursor, the point is over that character
  836. if (x < pixelCursor) {
  837. offset = i;
  838. break;
  839. }
  840. // Move between the current character and the next
  841. pixelCursor += step;
  842. }
  843. }
  844. // Creates a range with the text node of the element and set the offset found
  845. range.setStart(el.firstChild, offset);
  846. range.setEnd(el.firstChild, offset);
  847. }
  848. return range;
  849. }
  850. class CharWidthReader {
  851. constructor() {
  852. this._cache = {};
  853. this._canvas = document.createElement('canvas');
  854. }
  855. static getInstance() {
  856. if (!CharWidthReader._INSTANCE) {
  857. CharWidthReader._INSTANCE = new CharWidthReader();
  858. }
  859. return CharWidthReader._INSTANCE;
  860. }
  861. getCharWidth(char, font) {
  862. const cacheKey = char + font;
  863. if (this._cache[cacheKey]) {
  864. return this._cache[cacheKey];
  865. }
  866. const context = this._canvas.getContext('2d');
  867. context.font = font;
  868. const metrics = context.measureText(char);
  869. const width = metrics.width;
  870. this._cache[cacheKey] = width;
  871. return width;
  872. }
  873. }
  874. CharWidthReader._INSTANCE = null;