mouseTarget.js 39 KB

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