modesContentHover.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  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. var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
  6. var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
  7. if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
  8. else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
  9. return c > 3 && r && Object.defineProperty(target, key, r), r;
  10. };
  11. var __param = (this && this.__param) || function (paramIndex, decorator) {
  12. return function (target, key) { decorator(target, key, paramIndex); }
  13. };
  14. import * as dom from '../../../base/browser/dom.js';
  15. import { HoverAction, HoverWidget } from '../../../base/browser/ui/hover/hoverWidget.js';
  16. import { Widget } from '../../../base/browser/ui/widget.js';
  17. import { coalesce } from '../../../base/common/arrays.js';
  18. import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';
  19. import { Position } from '../../common/core/position.js';
  20. import { Range } from '../../common/core/range.js';
  21. import { ModelDecorationOptions } from '../../common/model/textModel.js';
  22. import { TokenizationRegistry } from '../../common/modes.js';
  23. import { ColorHoverParticipant } from './colorHoverParticipant.js';
  24. import { HoverOperation } from './hoverOperation.js';
  25. import { HoverRangeAnchor } from './hoverTypes.js';
  26. import { MarkdownHoverParticipant } from './markdownHoverParticipant.js';
  27. import { MarkerHoverParticipant } from './markerHoverParticipant.js';
  28. import { InlineCompletionsHoverParticipant } from '../inlineCompletions/inlineCompletionsHoverParticipant.js';
  29. import { IContextKeyService } from '../../../platform/contextkey/common/contextkey.js';
  30. import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';
  31. import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js';
  32. import { Context as SuggestContext } from '../suggest/suggest.js';
  33. import { UnicodeHighlighterHoverParticipant } from '../unicodeHighlighter/unicodeHighlighter.js';
  34. import { AsyncIterableObject } from '../../../base/common/async.js';
  35. const $ = dom.$;
  36. let EditorHoverStatusBar = class EditorHoverStatusBar extends Disposable {
  37. constructor(_keybindingService) {
  38. super();
  39. this._keybindingService = _keybindingService;
  40. this._hasContent = false;
  41. this.hoverElement = $('div.hover-row.status-bar');
  42. this.actionsElement = dom.append(this.hoverElement, $('div.actions'));
  43. }
  44. get hasContent() {
  45. return this._hasContent;
  46. }
  47. addAction(actionOptions) {
  48. const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId);
  49. const keybindingLabel = keybinding ? keybinding.getLabel() : null;
  50. this._hasContent = true;
  51. return this._register(HoverAction.render(this.actionsElement, actionOptions, keybindingLabel));
  52. }
  53. append(element) {
  54. const result = dom.append(this.actionsElement, element);
  55. this._hasContent = true;
  56. return result;
  57. }
  58. };
  59. EditorHoverStatusBar = __decorate([
  60. __param(0, IKeybindingService)
  61. ], EditorHoverStatusBar);
  62. class ModesContentComputer {
  63. constructor(editor, _participants) {
  64. this._participants = _participants;
  65. this._editor = editor;
  66. this._result = [];
  67. this._anchor = null;
  68. }
  69. setAnchor(anchor) {
  70. this._anchor = anchor;
  71. this._result = [];
  72. }
  73. clearResult() {
  74. this._result = [];
  75. }
  76. static _getLineDecorations(editor, anchor) {
  77. if (anchor.type !== 1 /* Range */) {
  78. return [];
  79. }
  80. const model = editor.getModel();
  81. const lineNumber = anchor.range.startLineNumber;
  82. const maxColumn = model.getLineMaxColumn(lineNumber);
  83. return editor.getLineDecorations(lineNumber).filter((d) => {
  84. if (d.options.isWholeLine) {
  85. return true;
  86. }
  87. const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1;
  88. const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn;
  89. if (d.options.showIfCollapsed) {
  90. // Relax check around `showIfCollapsed` decorations to also include +/- 1 character
  91. if (startColumn > anchor.range.startColumn + 1 || anchor.range.endColumn - 1 > endColumn) {
  92. return false;
  93. }
  94. }
  95. else {
  96. if (startColumn > anchor.range.startColumn || anchor.range.endColumn > endColumn) {
  97. return false;
  98. }
  99. }
  100. return true;
  101. });
  102. }
  103. computeAsync(token) {
  104. const anchor = this._anchor;
  105. if (!this._editor.hasModel() || !anchor) {
  106. return AsyncIterableObject.EMPTY;
  107. }
  108. const lineDecorations = ModesContentComputer._getLineDecorations(this._editor, anchor);
  109. return AsyncIterableObject.merge(this._participants.map(participant => this._computeAsync(participant, lineDecorations, anchor, token)));
  110. }
  111. _computeAsync(participant, lineDecorations, anchor, token) {
  112. if (!participant.computeAsync) {
  113. return AsyncIterableObject.EMPTY;
  114. }
  115. return participant.computeAsync(anchor, lineDecorations, token);
  116. }
  117. computeSync() {
  118. if (!this._editor.hasModel() || !this._anchor) {
  119. return [];
  120. }
  121. const lineDecorations = ModesContentComputer._getLineDecorations(this._editor, this._anchor);
  122. let result = [];
  123. for (const participant of this._participants) {
  124. result = result.concat(participant.computeSync(this._anchor, lineDecorations));
  125. }
  126. return coalesce(result);
  127. }
  128. onResult(result, isFromSynchronousComputation) {
  129. // Always put synchronous messages before asynchronous ones
  130. if (isFromSynchronousComputation) {
  131. this._result = result.concat(this._result);
  132. }
  133. else {
  134. this._result = this._result.concat(result);
  135. }
  136. }
  137. getResult() {
  138. return this._result.slice(0);
  139. }
  140. getResultWithLoadingMessage() {
  141. if (this._anchor) {
  142. for (const participant of this._participants) {
  143. if (participant.createLoadingMessage) {
  144. const loadingMessage = participant.createLoadingMessage(this._anchor);
  145. if (loadingMessage) {
  146. return this._result.slice(0).concat([loadingMessage]);
  147. }
  148. }
  149. }
  150. }
  151. return this._result.slice(0);
  152. }
  153. }
  154. let ModesContentHoverWidget = class ModesContentHoverWidget extends Widget {
  155. constructor(editor, _hoverVisibleKey, instantiationService, _keybindingService, _contextKeyService) {
  156. super();
  157. this._hoverVisibleKey = _hoverVisibleKey;
  158. this._keybindingService = _keybindingService;
  159. this._contextKeyService = _contextKeyService;
  160. // IContentWidget.allowEditorOverflow
  161. this.allowEditorOverflow = true;
  162. this._participants = [
  163. instantiationService.createInstance(ColorHoverParticipant, editor, this),
  164. instantiationService.createInstance(MarkdownHoverParticipant, editor, this),
  165. instantiationService.createInstance(InlineCompletionsHoverParticipant, editor, this),
  166. instantiationService.createInstance(UnicodeHighlighterHoverParticipant, editor, this),
  167. instantiationService.createInstance(MarkerHoverParticipant, editor, this),
  168. ];
  169. this._editor = editor;
  170. this._isVisible = false;
  171. this._stoleFocus = false;
  172. this._renderDisposable = null;
  173. this._hover = this._register(new HoverWidget());
  174. this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible);
  175. this.onkeydown(this._hover.containerDomNode, (e) => {
  176. if (e.equals(9 /* Escape */)) {
  177. this.hide();
  178. }
  179. });
  180. this._register(this._editor.onDidChangeConfiguration((e) => {
  181. if (e.hasChanged(43 /* fontInfo */)) {
  182. this._updateFont();
  183. }
  184. }));
  185. this._editor.onDidLayoutChange(() => this.layout());
  186. this.layout();
  187. this._editor.addContentWidget(this);
  188. this._showAtPosition = null;
  189. this._showAtRange = null;
  190. this._stoleFocus = false;
  191. this._messages = [];
  192. this._messagesAreComplete = false;
  193. this._lastAnchor = null;
  194. this._computer = new ModesContentComputer(this._editor, this._participants);
  195. this._highlightDecorations = [];
  196. this._isChangingDecorations = false;
  197. this._shouldFocus = false;
  198. this._colorPicker = null;
  199. this._preferAbove = this._editor.getOption(52 /* hover */).above;
  200. this._hoverOperation = new HoverOperation(this._computer, result => this._withResult(result, true), null, result => this._withResult(result, false), this._editor.getOption(52 /* hover */).delay);
  201. this._register(dom.addStandardDisposableListener(this.getDomNode(), dom.EventType.FOCUS, () => {
  202. if (this._colorPicker) {
  203. this.getDomNode().classList.add('colorpicker-hover');
  204. }
  205. }));
  206. this._register(dom.addStandardDisposableListener(this.getDomNode(), dom.EventType.BLUR, () => {
  207. this.getDomNode().classList.remove('colorpicker-hover');
  208. }));
  209. this._register(editor.onDidChangeConfiguration(() => {
  210. this._hoverOperation.setHoverTime(this._editor.getOption(52 /* hover */).delay);
  211. this._preferAbove = this._editor.getOption(52 /* hover */).above;
  212. }));
  213. this._register(TokenizationRegistry.onDidChange(() => {
  214. if (this._isVisible && this._lastAnchor && this._messages.length > 0) {
  215. this._hover.contentsDomNode.textContent = '';
  216. this._renderMessages(this._lastAnchor, this._messages);
  217. }
  218. }));
  219. }
  220. dispose() {
  221. this._hoverOperation.cancel();
  222. this._editor.removeContentWidget(this);
  223. super.dispose();
  224. }
  225. getId() {
  226. return ModesContentHoverWidget.ID;
  227. }
  228. getDomNode() {
  229. return this._hover.containerDomNode;
  230. }
  231. _shouldShowAt(mouseEvent) {
  232. const targetType = mouseEvent.target.type;
  233. if (targetType === 6 /* CONTENT_TEXT */) {
  234. return true;
  235. }
  236. if (targetType === 7 /* CONTENT_EMPTY */) {
  237. const epsilon = this._editor.getOption(43 /* fontInfo */).typicalHalfwidthCharacterWidth / 2;
  238. const data = mouseEvent.target.detail;
  239. if (data && !data.isAfterLines && typeof data.horizontalDistanceToText === 'number' && data.horizontalDistanceToText < epsilon) {
  240. // Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough
  241. return true;
  242. }
  243. }
  244. return false;
  245. }
  246. maybeShowAt(mouseEvent) {
  247. var _a;
  248. const anchorCandidates = [];
  249. for (const participant of this._participants) {
  250. if (typeof participant.suggestHoverAnchor === 'function') {
  251. const anchor = participant.suggestHoverAnchor(mouseEvent);
  252. if (anchor) {
  253. anchorCandidates.push(anchor);
  254. }
  255. }
  256. }
  257. if (this._shouldShowAt(mouseEvent) && mouseEvent.target.range) {
  258. // TODO@rebornix. This should be removed if we move Color Picker out of Hover component.
  259. // Check if mouse is hovering on color decorator
  260. const hoverOnColorDecorator = [...((_a = mouseEvent.target.element) === null || _a === void 0 ? void 0 : _a.classList.values()) || []].find(className => className.startsWith('ced-colorBox'))
  261. && mouseEvent.target.range.endColumn - mouseEvent.target.range.startColumn === 1;
  262. const showAtRange = (hoverOnColorDecorator // shift the mouse focus by one as color decorator is a `before` decoration of next character.
  263. ? new Range(mouseEvent.target.range.startLineNumber, mouseEvent.target.range.startColumn + 1, mouseEvent.target.range.endLineNumber, mouseEvent.target.range.endColumn + 1)
  264. : mouseEvent.target.range);
  265. anchorCandidates.push(new HoverRangeAnchor(0, showAtRange));
  266. }
  267. if (anchorCandidates.length === 0) {
  268. return false;
  269. }
  270. anchorCandidates.sort((a, b) => b.priority - a.priority);
  271. this._startShowingAt(anchorCandidates[0], 0 /* Delayed */, false);
  272. return true;
  273. }
  274. _showAt(position, range, focus) {
  275. // Position has changed
  276. this._showAtPosition = position;
  277. this._showAtRange = range;
  278. this._hoverVisibleKey.set(true);
  279. this._isVisible = true;
  280. this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible);
  281. this._editor.layoutContentWidget(this);
  282. // Simply force a synchronous render on the editor
  283. // such that the widget does not really render with left = '0px'
  284. this._editor.render();
  285. this._stoleFocus = focus;
  286. if (focus) {
  287. this._hover.containerDomNode.focus();
  288. }
  289. }
  290. getPosition() {
  291. if (this._isVisible) {
  292. let preferAbove = this._preferAbove;
  293. if (!preferAbove && this._contextKeyService.getContextKeyValue(SuggestContext.Visible.key)) {
  294. // Prefer rendering above if the suggest widget is visible
  295. preferAbove = true;
  296. }
  297. return {
  298. position: this._showAtPosition,
  299. range: this._showAtRange,
  300. preference: preferAbove ? [
  301. 1 /* ABOVE */,
  302. 2 /* BELOW */,
  303. ] : [
  304. 2 /* BELOW */,
  305. 1 /* ABOVE */,
  306. ],
  307. };
  308. }
  309. return null;
  310. }
  311. _updateFont() {
  312. const codeClasses = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code'));
  313. codeClasses.forEach(node => this._editor.applyFontInfo(node));
  314. }
  315. _updateContents(node) {
  316. this._hover.contentsDomNode.textContent = '';
  317. this._hover.contentsDomNode.appendChild(node);
  318. this._updateFont();
  319. this._editor.layoutContentWidget(this);
  320. this._hover.onContentsChanged();
  321. }
  322. layout() {
  323. const height = Math.max(this._editor.getLayoutInfo().height / 4, 250);
  324. const { fontSize, lineHeight } = this._editor.getOption(43 /* fontInfo */);
  325. this._hover.contentsDomNode.style.fontSize = `${fontSize}px`;
  326. this._hover.contentsDomNode.style.lineHeight = `${lineHeight / fontSize}`;
  327. this._hover.contentsDomNode.style.maxHeight = `${height}px`;
  328. this._hover.contentsDomNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`;
  329. }
  330. onModelDecorationsChanged() {
  331. if (this._isChangingDecorations) {
  332. return;
  333. }
  334. if (this._isVisible) {
  335. // The decorations have changed and the hover is visible,
  336. // we need to recompute the displayed text
  337. this._hoverOperation.cancel();
  338. this._computer.clearResult();
  339. if (!this._colorPicker) { // TODO@Michel ensure that displayed text for other decorations is computed even if color picker is in place
  340. this._hoverOperation.start(0 /* Delayed */);
  341. }
  342. }
  343. }
  344. startShowingAtRange(range, mode, focus) {
  345. this._startShowingAt(new HoverRangeAnchor(0, range), mode, focus);
  346. }
  347. _startShowingAt(anchor, mode, focus) {
  348. if (this._lastAnchor && this._lastAnchor.equals(anchor)) {
  349. // We have to show the widget at the exact same range as before, so no work is needed
  350. return;
  351. }
  352. this._hoverOperation.cancel();
  353. if (this._isVisible) {
  354. // The range might have changed, but the hover is visible
  355. // Instead of hiding it completely, filter out messages that are still in the new range and
  356. // kick off a new computation
  357. if (!this._showAtPosition || !this._lastAnchor || !anchor.canAdoptVisibleHover(this._lastAnchor, this._showAtPosition)) {
  358. this.hide();
  359. }
  360. else {
  361. const filteredMessages = this._messages.filter((m) => m.isValidForHoverAnchor(anchor));
  362. if (filteredMessages.length === 0) {
  363. this.hide();
  364. }
  365. else if (filteredMessages.length === this._messages.length && this._messagesAreComplete) {
  366. // no change
  367. return;
  368. }
  369. else {
  370. this._renderMessages(anchor, filteredMessages);
  371. }
  372. }
  373. }
  374. this._lastAnchor = anchor;
  375. this._computer.setAnchor(anchor);
  376. this._shouldFocus = focus;
  377. this._hoverOperation.start(mode);
  378. }
  379. hide() {
  380. this._lastAnchor = null;
  381. this._hoverOperation.cancel();
  382. if (this._isVisible) {
  383. setTimeout(() => {
  384. // Give commands a chance to see the key
  385. if (!this._isVisible) {
  386. this._hoverVisibleKey.set(false);
  387. }
  388. }, 0);
  389. this._isVisible = false;
  390. this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible);
  391. this._editor.layoutContentWidget(this);
  392. if (this._stoleFocus) {
  393. this._editor.focus();
  394. }
  395. }
  396. this._isChangingDecorations = true;
  397. this._highlightDecorations = this._editor.deltaDecorations(this._highlightDecorations, []);
  398. this._isChangingDecorations = false;
  399. if (this._renderDisposable) {
  400. this._renderDisposable.dispose();
  401. this._renderDisposable = null;
  402. }
  403. this._colorPicker = null;
  404. }
  405. isColorPickerVisible() {
  406. return !!this._colorPicker;
  407. }
  408. setColorPicker(widget) {
  409. this._colorPicker = widget;
  410. }
  411. onContentsChanged() {
  412. this._hover.onContentsChanged();
  413. }
  414. _withResult(result, complete) {
  415. this._messages = result;
  416. this._messagesAreComplete = complete;
  417. if (this._lastAnchor && this._messages.length > 0) {
  418. this._renderMessages(this._lastAnchor, this._messages);
  419. }
  420. else if (complete) {
  421. this.hide();
  422. }
  423. }
  424. _renderMessages(anchor, messages) {
  425. if (this._renderDisposable) {
  426. this._renderDisposable.dispose();
  427. this._renderDisposable = null;
  428. }
  429. this._colorPicker = null; // TODO: TypeScript thinks this is always null
  430. // update column from which to show
  431. let renderColumn = 1073741824 /* MAX_SAFE_SMALL_INTEGER */;
  432. let highlightRange = messages[0].range;
  433. let forceShowAtRange = null;
  434. let fragment = document.createDocumentFragment();
  435. const disposables = new DisposableStore();
  436. const hoverParts = new Map();
  437. for (const msg of messages) {
  438. renderColumn = Math.min(renderColumn, msg.range.startColumn);
  439. highlightRange = Range.plusRange(highlightRange, msg.range);
  440. if (msg.forceShowAtRange) {
  441. forceShowAtRange = msg.range;
  442. }
  443. if (!hoverParts.has(msg.owner)) {
  444. hoverParts.set(msg.owner, []);
  445. }
  446. const dest = hoverParts.get(msg.owner);
  447. dest.push(msg);
  448. }
  449. const statusBar = disposables.add(new EditorHoverStatusBar(this._keybindingService));
  450. for (const participant of this._participants) {
  451. if (hoverParts.has(participant)) {
  452. const participantHoverParts = hoverParts.get(participant);
  453. disposables.add(participant.renderHoverParts(participantHoverParts, fragment, statusBar));
  454. }
  455. }
  456. if (statusBar.hasContent) {
  457. fragment.appendChild(statusBar.hoverElement);
  458. }
  459. this._renderDisposable = disposables;
  460. // show
  461. if (fragment.hasChildNodes()) {
  462. if (forceShowAtRange) {
  463. this._showAt(forceShowAtRange.getStartPosition(), forceShowAtRange, this._shouldFocus);
  464. }
  465. else {
  466. this._showAt(new Position(anchor.range.startLineNumber, renderColumn), highlightRange, this._shouldFocus);
  467. }
  468. this._updateContents(fragment);
  469. }
  470. if (this._colorPicker) {
  471. this._colorPicker.layout();
  472. }
  473. this._isChangingDecorations = true;
  474. this._highlightDecorations = this._editor.deltaDecorations(this._highlightDecorations, highlightRange ? [{
  475. range: highlightRange,
  476. options: ModesContentHoverWidget._DECORATION_OPTIONS
  477. }] : []);
  478. this._isChangingDecorations = false;
  479. }
  480. };
  481. ModesContentHoverWidget.ID = 'editor.contrib.modesContentHoverWidget';
  482. ModesContentHoverWidget._DECORATION_OPTIONS = ModelDecorationOptions.register({
  483. description: 'content-hover-highlight',
  484. className: 'hoverHighlight'
  485. });
  486. ModesContentHoverWidget = __decorate([
  487. __param(2, IInstantiationService),
  488. __param(3, IKeybindingService),
  489. __param(4, IContextKeyService)
  490. ], ModesContentHoverWidget);
  491. export { ModesContentHoverWidget };