0d4f61ea37cf23fc8eda55244f87d3814500c4095dcd24263d78d4413041de2c2af9b3301d1a9a7d66be5e8476e4ea13a760d1d045791fde00c3733afb1df1 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  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 { coalesce } from '../../../../base/common/arrays.js';
  17. import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
  18. import { Position } from '../../../common/core/position.js';
  19. import { Range } from '../../../common/core/range.js';
  20. import { ModelDecorationOptions } from '../../../common/model/textModel.js';
  21. import { TokenizationRegistry } from '../../../common/languages.js';
  22. import { HoverOperation } from './hoverOperation.js';
  23. import { HoverParticipantRegistry, HoverRangeAnchor } from './hoverTypes.js';
  24. import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
  25. import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
  26. import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
  27. import { Context as SuggestContext } from '../../suggest/browser/suggest.js';
  28. import { AsyncIterableObject } from '../../../../base/common/async.js';
  29. import { EditorContextKeys } from '../../../common/editorContextKeys.js';
  30. const $ = dom.$;
  31. let ContentHoverController = class ContentHoverController extends Disposable {
  32. constructor(_editor, _instantiationService, _keybindingService) {
  33. super();
  34. this._editor = _editor;
  35. this._instantiationService = _instantiationService;
  36. this._keybindingService = _keybindingService;
  37. this._widget = this._register(this._instantiationService.createInstance(ContentHoverWidget, this._editor));
  38. this._isChangingDecorations = false;
  39. this._messages = [];
  40. this._messagesAreComplete = false;
  41. // Instantiate participants and sort them by `hoverOrdinal` which is relevant for rendering order.
  42. this._participants = [];
  43. for (const participant of HoverParticipantRegistry.getAll()) {
  44. this._participants.push(this._instantiationService.createInstance(participant, this._editor));
  45. }
  46. this._participants.sort((p1, p2) => p1.hoverOrdinal - p2.hoverOrdinal);
  47. this._computer = new ContentHoverComputer(this._editor, this._participants);
  48. this._hoverOperation = this._register(new HoverOperation(this._editor, this._computer));
  49. this._register(this._hoverOperation.onResult((result) => {
  50. this._withResult(result.value, result.isComplete, result.hasLoadingMessage);
  51. }));
  52. this._register(this._editor.onDidChangeModelDecorations(() => {
  53. if (this._isChangingDecorations) {
  54. return;
  55. }
  56. this._onModelDecorationsChanged();
  57. }));
  58. this._register(dom.addStandardDisposableListener(this._widget.getDomNode(), 'keydown', (e) => {
  59. if (e.equals(9 /* KeyCode.Escape */)) {
  60. this.hide();
  61. }
  62. }));
  63. this._register(TokenizationRegistry.onDidChange(() => {
  64. if (this._widget.position && this._computer.anchor && this._messages.length > 0) {
  65. this._widget.clear();
  66. this._renderMessages(this._computer.anchor, this._messages);
  67. }
  68. }));
  69. }
  70. _onModelDecorationsChanged() {
  71. if (this._widget.position) {
  72. // The decorations have changed and the hover is visible,
  73. // we need to recompute the displayed text
  74. this._hoverOperation.cancel();
  75. if (!this._widget.isColorPickerVisible) { // TODO@Michel ensure that displayed text for other decorations is computed even if color picker is in place
  76. this._hoverOperation.start(0 /* HoverStartMode.Delayed */);
  77. }
  78. }
  79. }
  80. maybeShowAt(mouseEvent) {
  81. const anchorCandidates = [];
  82. for (const participant of this._participants) {
  83. if (participant.suggestHoverAnchor) {
  84. const anchor = participant.suggestHoverAnchor(mouseEvent);
  85. if (anchor) {
  86. anchorCandidates.push(anchor);
  87. }
  88. }
  89. }
  90. const target = mouseEvent.target;
  91. if (target.type === 6 /* MouseTargetType.CONTENT_TEXT */) {
  92. anchorCandidates.push(new HoverRangeAnchor(0, target.range));
  93. }
  94. if (target.type === 7 /* MouseTargetType.CONTENT_EMPTY */) {
  95. const epsilon = this._editor.getOption(46 /* EditorOption.fontInfo */).typicalHalfwidthCharacterWidth / 2;
  96. if (!target.detail.isAfterLines && typeof target.detail.horizontalDistanceToText === 'number' && target.detail.horizontalDistanceToText < epsilon) {
  97. // Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough
  98. anchorCandidates.push(new HoverRangeAnchor(0, target.range));
  99. }
  100. }
  101. if (anchorCandidates.length === 0) {
  102. return false;
  103. }
  104. anchorCandidates.sort((a, b) => b.priority - a.priority);
  105. this._startShowingAt(anchorCandidates[0], 0 /* HoverStartMode.Delayed */, false);
  106. return true;
  107. }
  108. startShowingAtRange(range, mode, focus) {
  109. this._startShowingAt(new HoverRangeAnchor(0, range), mode, focus);
  110. }
  111. _startShowingAt(anchor, mode, focus) {
  112. if (this._computer.anchor && this._computer.anchor.equals(anchor)) {
  113. // We have to show the widget at the exact same range as before, so no work is needed
  114. return;
  115. }
  116. this._hoverOperation.cancel();
  117. if (this._widget.position) {
  118. // The range might have changed, but the hover is visible
  119. // Instead of hiding it completely, filter out messages that are still in the new range and
  120. // kick off a new computation
  121. if (!this._computer.anchor || !anchor.canAdoptVisibleHover(this._computer.anchor, this._widget.position)) {
  122. this.hide();
  123. }
  124. else {
  125. const filteredMessages = this._messages.filter((m) => m.isValidForHoverAnchor(anchor));
  126. if (filteredMessages.length === 0) {
  127. this.hide();
  128. }
  129. else if (filteredMessages.length === this._messages.length && this._messagesAreComplete) {
  130. // no change
  131. return;
  132. }
  133. else {
  134. this._renderMessages(anchor, filteredMessages);
  135. }
  136. }
  137. }
  138. this._computer.anchor = anchor;
  139. this._computer.shouldFocus = focus;
  140. this._hoverOperation.start(mode);
  141. }
  142. hide() {
  143. this._computer.anchor = null;
  144. this._hoverOperation.cancel();
  145. this._widget.hide();
  146. }
  147. isColorPickerVisible() {
  148. return this._widget.isColorPickerVisible;
  149. }
  150. containsNode(node) {
  151. return this._widget.getDomNode().contains(node);
  152. }
  153. _addLoadingMessage(result) {
  154. if (this._computer.anchor) {
  155. for (const participant of this._participants) {
  156. if (participant.createLoadingMessage) {
  157. const loadingMessage = participant.createLoadingMessage(this._computer.anchor);
  158. if (loadingMessage) {
  159. return result.slice(0).concat([loadingMessage]);
  160. }
  161. }
  162. }
  163. }
  164. return result;
  165. }
  166. _withResult(result, isComplete, hasLoadingMessage) {
  167. this._messages = (hasLoadingMessage ? this._addLoadingMessage(result) : result);
  168. this._messagesAreComplete = isComplete;
  169. if (this._computer.anchor && this._messages.length > 0) {
  170. this._renderMessages(this._computer.anchor, this._messages);
  171. }
  172. else if (isComplete) {
  173. this.hide();
  174. }
  175. }
  176. _renderMessages(anchor, messages) {
  177. const { showAtPosition, showAtRange, highlightRange } = ContentHoverController.computeHoverRanges(anchor.range, messages);
  178. const disposables = new DisposableStore();
  179. const statusBar = disposables.add(new EditorHoverStatusBar(this._keybindingService));
  180. const fragment = document.createDocumentFragment();
  181. let colorPicker = null;
  182. const context = {
  183. fragment,
  184. statusBar,
  185. setColorPicker: (widget) => colorPicker = widget,
  186. onContentsChanged: () => this._widget.onContentsChanged(),
  187. hide: () => this.hide()
  188. };
  189. for (const participant of this._participants) {
  190. const hoverParts = messages.filter(msg => msg.owner === participant);
  191. if (hoverParts.length > 0) {
  192. disposables.add(participant.renderHoverParts(context, hoverParts));
  193. }
  194. }
  195. if (statusBar.hasContent) {
  196. fragment.appendChild(statusBar.hoverElement);
  197. }
  198. if (fragment.hasChildNodes()) {
  199. if (highlightRange) {
  200. const highlightDecoration = this._editor.createDecorationsCollection();
  201. try {
  202. this._isChangingDecorations = true;
  203. highlightDecoration.set([{
  204. range: highlightRange,
  205. options: ContentHoverController._DECORATION_OPTIONS
  206. }]);
  207. }
  208. finally {
  209. this._isChangingDecorations = false;
  210. }
  211. disposables.add(toDisposable(() => {
  212. try {
  213. this._isChangingDecorations = true;
  214. highlightDecoration.clear();
  215. }
  216. finally {
  217. this._isChangingDecorations = false;
  218. }
  219. }));
  220. }
  221. this._widget.showAt(fragment, new ContentHoverVisibleData(colorPicker, showAtPosition, showAtRange, this._editor.getOption(55 /* EditorOption.hover */).above, this._computer.shouldFocus, disposables));
  222. }
  223. else {
  224. disposables.dispose();
  225. }
  226. }
  227. static computeHoverRanges(anchorRange, messages) {
  228. // The anchor range is always on a single line
  229. const anchorLineNumber = anchorRange.startLineNumber;
  230. let renderStartColumn = anchorRange.startColumn;
  231. let renderEndColumn = anchorRange.endColumn;
  232. let highlightRange = messages[0].range;
  233. let forceShowAtRange = null;
  234. for (const msg of messages) {
  235. highlightRange = Range.plusRange(highlightRange, msg.range);
  236. if (msg.range.startLineNumber === anchorLineNumber && msg.range.endLineNumber === anchorLineNumber) {
  237. // this message has a range that is completely sitting on the line of the anchor
  238. renderStartColumn = Math.min(renderStartColumn, msg.range.startColumn);
  239. renderEndColumn = Math.max(renderEndColumn, msg.range.endColumn);
  240. }
  241. if (msg.forceShowAtRange) {
  242. forceShowAtRange = msg.range;
  243. }
  244. }
  245. return {
  246. showAtPosition: forceShowAtRange ? forceShowAtRange.getStartPosition() : new Position(anchorRange.startLineNumber, renderStartColumn),
  247. showAtRange: forceShowAtRange ? forceShowAtRange : new Range(anchorLineNumber, renderStartColumn, anchorLineNumber, renderEndColumn),
  248. highlightRange
  249. };
  250. }
  251. };
  252. ContentHoverController._DECORATION_OPTIONS = ModelDecorationOptions.register({
  253. description: 'content-hover-highlight',
  254. className: 'hoverHighlight'
  255. });
  256. ContentHoverController = __decorate([
  257. __param(1, IInstantiationService),
  258. __param(2, IKeybindingService)
  259. ], ContentHoverController);
  260. export { ContentHoverController };
  261. class ContentHoverVisibleData {
  262. constructor(colorPicker, showAtPosition, showAtRange, preferAbove, stoleFocus, disposables) {
  263. this.colorPicker = colorPicker;
  264. this.showAtPosition = showAtPosition;
  265. this.showAtRange = showAtRange;
  266. this.preferAbove = preferAbove;
  267. this.stoleFocus = stoleFocus;
  268. this.disposables = disposables;
  269. }
  270. }
  271. let ContentHoverWidget = class ContentHoverWidget extends Disposable {
  272. constructor(_editor, _contextKeyService) {
  273. super();
  274. this._editor = _editor;
  275. this._contextKeyService = _contextKeyService;
  276. this.allowEditorOverflow = true;
  277. this._hoverVisibleKey = EditorContextKeys.hoverVisible.bindTo(this._contextKeyService);
  278. this._hover = this._register(new HoverWidget());
  279. this._visibleData = null;
  280. this._register(this._editor.onDidLayoutChange(() => this._layout()));
  281. this._register(this._editor.onDidChangeConfiguration((e) => {
  282. if (e.hasChanged(46 /* EditorOption.fontInfo */)) {
  283. this._updateFont();
  284. }
  285. }));
  286. this._setVisibleData(null);
  287. this._layout();
  288. this._editor.addContentWidget(this);
  289. }
  290. /**
  291. * Returns `null` if the hover is not visible.
  292. */
  293. get position() {
  294. var _a, _b;
  295. return (_b = (_a = this._visibleData) === null || _a === void 0 ? void 0 : _a.showAtPosition) !== null && _b !== void 0 ? _b : null;
  296. }
  297. get isColorPickerVisible() {
  298. var _a;
  299. return Boolean((_a = this._visibleData) === null || _a === void 0 ? void 0 : _a.colorPicker);
  300. }
  301. dispose() {
  302. this._editor.removeContentWidget(this);
  303. if (this._visibleData) {
  304. this._visibleData.disposables.dispose();
  305. }
  306. super.dispose();
  307. }
  308. getId() {
  309. return ContentHoverWidget.ID;
  310. }
  311. getDomNode() {
  312. return this._hover.containerDomNode;
  313. }
  314. getPosition() {
  315. if (!this._visibleData) {
  316. return null;
  317. }
  318. let preferAbove = this._visibleData.preferAbove;
  319. if (!preferAbove && this._contextKeyService.getContextKeyValue(SuggestContext.Visible.key)) {
  320. // Prefer rendering above if the suggest widget is visible
  321. preferAbove = true;
  322. }
  323. return {
  324. position: this._visibleData.showAtPosition,
  325. range: this._visibleData.showAtRange,
  326. preference: (preferAbove
  327. ? [1 /* ContentWidgetPositionPreference.ABOVE */, 2 /* ContentWidgetPositionPreference.BELOW */]
  328. : [2 /* ContentWidgetPositionPreference.BELOW */, 1 /* ContentWidgetPositionPreference.ABOVE */]),
  329. };
  330. }
  331. _setVisibleData(visibleData) {
  332. if (this._visibleData) {
  333. this._visibleData.disposables.dispose();
  334. }
  335. this._visibleData = visibleData;
  336. this._hoverVisibleKey.set(!!this._visibleData);
  337. this._hover.containerDomNode.classList.toggle('hidden', !this._visibleData);
  338. }
  339. _layout() {
  340. const height = Math.max(this._editor.getLayoutInfo().height / 4, 250);
  341. const { fontSize, lineHeight } = this._editor.getOption(46 /* EditorOption.fontInfo */);
  342. this._hover.contentsDomNode.style.fontSize = `${fontSize}px`;
  343. this._hover.contentsDomNode.style.lineHeight = `${lineHeight / fontSize}`;
  344. this._hover.contentsDomNode.style.maxHeight = `${height}px`;
  345. this._hover.contentsDomNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`;
  346. }
  347. _updateFont() {
  348. const codeClasses = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code'));
  349. codeClasses.forEach(node => this._editor.applyFontInfo(node));
  350. }
  351. showAt(node, visibleData) {
  352. this._setVisibleData(visibleData);
  353. this._hover.contentsDomNode.textContent = '';
  354. this._hover.contentsDomNode.appendChild(node);
  355. this._hover.contentsDomNode.style.paddingBottom = '';
  356. this._updateFont();
  357. this.onContentsChanged();
  358. // Simply force a synchronous render on the editor
  359. // such that the widget does not really render with left = '0px'
  360. this._editor.render();
  361. // See https://github.com/microsoft/vscode/issues/140339
  362. // TODO: Doing a second layout of the hover after force rendering the editor
  363. this.onContentsChanged();
  364. if (visibleData.stoleFocus) {
  365. this._hover.containerDomNode.focus();
  366. }
  367. if (visibleData.colorPicker) {
  368. visibleData.colorPicker.layout();
  369. }
  370. }
  371. hide() {
  372. if (this._visibleData) {
  373. const stoleFocus = this._visibleData.stoleFocus;
  374. this._setVisibleData(null);
  375. this._editor.layoutContentWidget(this);
  376. if (stoleFocus) {
  377. this._editor.focus();
  378. }
  379. }
  380. }
  381. onContentsChanged() {
  382. this._editor.layoutContentWidget(this);
  383. this._hover.onContentsChanged();
  384. const scrollDimensions = this._hover.scrollbar.getScrollDimensions();
  385. const hasHorizontalScrollbar = (scrollDimensions.scrollWidth > scrollDimensions.width);
  386. if (hasHorizontalScrollbar) {
  387. // There is just a horizontal scrollbar
  388. const extraBottomPadding = `${this._hover.scrollbar.options.horizontalScrollbarSize}px`;
  389. if (this._hover.contentsDomNode.style.paddingBottom !== extraBottomPadding) {
  390. this._hover.contentsDomNode.style.paddingBottom = extraBottomPadding;
  391. this._editor.layoutContentWidget(this);
  392. this._hover.onContentsChanged();
  393. }
  394. }
  395. }
  396. clear() {
  397. this._hover.contentsDomNode.textContent = '';
  398. }
  399. };
  400. ContentHoverWidget.ID = 'editor.contrib.contentHoverWidget';
  401. ContentHoverWidget = __decorate([
  402. __param(1, IContextKeyService)
  403. ], ContentHoverWidget);
  404. export { ContentHoverWidget };
  405. let EditorHoverStatusBar = class EditorHoverStatusBar extends Disposable {
  406. constructor(_keybindingService) {
  407. super();
  408. this._keybindingService = _keybindingService;
  409. this._hasContent = false;
  410. this.hoverElement = $('div.hover-row.status-bar');
  411. this.actionsElement = dom.append(this.hoverElement, $('div.actions'));
  412. }
  413. get hasContent() {
  414. return this._hasContent;
  415. }
  416. addAction(actionOptions) {
  417. const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId);
  418. const keybindingLabel = keybinding ? keybinding.getLabel() : null;
  419. this._hasContent = true;
  420. return this._register(HoverAction.render(this.actionsElement, actionOptions, keybindingLabel));
  421. }
  422. append(element) {
  423. const result = dom.append(this.actionsElement, element);
  424. this._hasContent = true;
  425. return result;
  426. }
  427. };
  428. EditorHoverStatusBar = __decorate([
  429. __param(0, IKeybindingService)
  430. ], EditorHoverStatusBar);
  431. class ContentHoverComputer {
  432. constructor(_editor, _participants) {
  433. this._editor = _editor;
  434. this._participants = _participants;
  435. this._anchor = null;
  436. this._shouldFocus = false;
  437. }
  438. get anchor() { return this._anchor; }
  439. set anchor(value) { this._anchor = value; }
  440. get shouldFocus() { return this._shouldFocus; }
  441. set shouldFocus(value) { this._shouldFocus = value; }
  442. static _getLineDecorations(editor, anchor) {
  443. if (anchor.type !== 1 /* HoverAnchorType.Range */) {
  444. return [];
  445. }
  446. const model = editor.getModel();
  447. const lineNumber = anchor.range.startLineNumber;
  448. if (lineNumber > model.getLineCount()) {
  449. // invalid line
  450. return [];
  451. }
  452. const maxColumn = model.getLineMaxColumn(lineNumber);
  453. return editor.getLineDecorations(lineNumber).filter((d) => {
  454. if (d.options.isWholeLine) {
  455. return true;
  456. }
  457. const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1;
  458. const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn;
  459. if (d.options.showIfCollapsed) {
  460. // Relax check around `showIfCollapsed` decorations to also include +/- 1 character
  461. if (startColumn > anchor.range.startColumn + 1 || anchor.range.endColumn - 1 > endColumn) {
  462. return false;
  463. }
  464. }
  465. else {
  466. if (startColumn > anchor.range.startColumn || anchor.range.endColumn > endColumn) {
  467. return false;
  468. }
  469. }
  470. return true;
  471. });
  472. }
  473. computeAsync(token) {
  474. const anchor = this._anchor;
  475. if (!this._editor.hasModel() || !anchor) {
  476. return AsyncIterableObject.EMPTY;
  477. }
  478. const lineDecorations = ContentHoverComputer._getLineDecorations(this._editor, anchor);
  479. return AsyncIterableObject.merge(this._participants.map((participant) => {
  480. if (!participant.computeAsync) {
  481. return AsyncIterableObject.EMPTY;
  482. }
  483. return participant.computeAsync(anchor, lineDecorations, token);
  484. }));
  485. }
  486. computeSync() {
  487. if (!this._editor.hasModel() || !this._anchor) {
  488. return [];
  489. }
  490. const lineDecorations = ContentHoverComputer._getLineDecorations(this._editor, this._anchor);
  491. let result = [];
  492. for (const participant of this._participants) {
  493. result = result.concat(participant.computeSync(this._anchor, lineDecorations));
  494. }
  495. return coalesce(result);
  496. }
  497. }