inlayHintsController.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  6. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  7. return new (P || (P = Promise))(function (resolve, reject) {
  8. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  9. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  10. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  11. step((generator = generator.apply(thisArg, _arguments || [])).next());
  12. });
  13. };
  14. import { RunOnceScheduler } from '../../../base/common/async.js';
  15. import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
  16. import { onUnexpectedExternalError } from '../../../base/common/errors.js';
  17. import { DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
  18. import { LRUCache, ResourceMap } from '../../../base/common/map.js';
  19. import { assertType } from '../../../base/common/types.js';
  20. import { URI } from '../../../base/common/uri.js';
  21. import { DynamicCssRules } from '../../browser/editorDom.js';
  22. import { registerEditorContribution } from '../../browser/editorExtensions.js';
  23. import { EDITOR_FONT_DEFAULTS } from '../../common/config/editorOptions.js';
  24. import { Position } from '../../common/core/position.js';
  25. import { Range } from '../../common/core/range.js';
  26. import { InlayHintKind, InlayHintsProviderRegistry } from '../../common/modes.js';
  27. import { LanguageFeatureRequestDelays } from '../../common/modes/languageFeatureRegistry.js';
  28. import { ITextModelService } from '../../common/services/resolverService.js';
  29. import { CommandsRegistry } from '../../../platform/commands/common/commands.js';
  30. import { editorInlayHintBackground, editorInlayHintForeground, editorInlayHintParameterBackground, editorInlayHintParameterForeground, editorInlayHintTypeBackground, editorInlayHintTypeForeground } from '../../../platform/theme/common/colorRegistry.js';
  31. import { themeColorFromId } from '../../../platform/theme/common/themeService.js';
  32. const MAX_DECORATORS = 1500;
  33. class RequestMap {
  34. constructor() {
  35. this._data = new ResourceMap();
  36. }
  37. push(model, provider) {
  38. const value = this._data.get(model.uri);
  39. if (value === undefined) {
  40. this._data.set(model.uri, new Set([provider]));
  41. }
  42. else {
  43. value.add(provider);
  44. }
  45. }
  46. pop(model, provider) {
  47. const value = this._data.get(model.uri);
  48. if (value) {
  49. value.delete(provider);
  50. if (value.size === 0) {
  51. this._data.delete(model.uri);
  52. }
  53. }
  54. }
  55. has(model, provider) {
  56. var _a;
  57. return Boolean((_a = this._data.get(model.uri)) === null || _a === void 0 ? void 0 : _a.has(provider));
  58. }
  59. }
  60. export function getInlayHints(model, ranges, requests, token) {
  61. return __awaiter(this, void 0, void 0, function* () {
  62. const all = [];
  63. const providers = InlayHintsProviderRegistry.ordered(model).reverse();
  64. const promises = providers.map(provider => ranges.map((range) => __awaiter(this, void 0, void 0, function* () {
  65. try {
  66. requests.push(model, provider);
  67. const result = yield provider.provideInlayHints(model, range, token);
  68. if (result === null || result === void 0 ? void 0 : result.length) {
  69. all.push(result.filter(hint => range.containsPosition(hint.position)));
  70. }
  71. }
  72. catch (err) {
  73. onUnexpectedExternalError(err);
  74. }
  75. finally {
  76. requests.pop(model, provider);
  77. }
  78. })));
  79. yield Promise.all(promises.flat());
  80. return all.flat().sort((a, b) => Position.compare(a.position, b.position));
  81. });
  82. }
  83. class InlayHintsCache {
  84. constructor() {
  85. this._entries = new LRUCache(50);
  86. }
  87. get(model) {
  88. const key = InlayHintsCache._key(model);
  89. return this._entries.get(key);
  90. }
  91. set(model, value) {
  92. const key = InlayHintsCache._key(model);
  93. this._entries.set(key, value);
  94. }
  95. static _key(model) {
  96. return `${model.uri.toString()}/${model.getVersionId()}`;
  97. }
  98. }
  99. export class InlayHintsController {
  100. constructor(_editor) {
  101. this._editor = _editor;
  102. this._decorationOwnerId = ++InlayHintsController._decorationOwnerIdPool;
  103. this._disposables = new DisposableStore();
  104. this._sessionDisposables = new DisposableStore();
  105. this._getInlayHintsDelays = new LanguageFeatureRequestDelays(InlayHintsProviderRegistry, 25, 500);
  106. this._cache = new InlayHintsCache();
  107. this._decorationsMetadata = new Map();
  108. this._ruleFactory = new DynamicCssRules(this._editor);
  109. this._disposables.add(InlayHintsProviderRegistry.onDidChange(() => this._update()));
  110. this._disposables.add(_editor.onDidChangeModel(() => this._update()));
  111. this._disposables.add(_editor.onDidChangeModelLanguage(() => this._update()));
  112. this._disposables.add(_editor.onDidChangeConfiguration(e => {
  113. if (e.hasChanged(126 /* inlayHints */)) {
  114. this._update();
  115. }
  116. }));
  117. this._update();
  118. }
  119. dispose() {
  120. this._sessionDisposables.dispose();
  121. this._removeAllDecorations();
  122. this._disposables.dispose();
  123. }
  124. _update() {
  125. this._sessionDisposables.clear();
  126. this._removeAllDecorations();
  127. if (!this._editor.getOption(126 /* inlayHints */).enabled) {
  128. return;
  129. }
  130. const model = this._editor.getModel();
  131. if (!model || !InlayHintsProviderRegistry.has(model)) {
  132. return;
  133. }
  134. // iff possible, quickly update from cache
  135. const cached = this._cache.get(model);
  136. if (cached) {
  137. this._updateHintsDecorators([model.getFullModelRange()], cached);
  138. }
  139. const requests = new RequestMap();
  140. const scheduler = new RunOnceScheduler(() => __awaiter(this, void 0, void 0, function* () {
  141. const t1 = Date.now();
  142. const cts = new CancellationTokenSource();
  143. this._sessionDisposables.add(toDisposable(() => cts.dispose(true)));
  144. const ranges = this._getHintsRanges();
  145. const result = yield getInlayHints(model, ranges, requests, cts.token);
  146. scheduler.delay = this._getInlayHintsDelays.update(model, Date.now() - t1);
  147. if (cts.token.isCancellationRequested) {
  148. return;
  149. }
  150. this._updateHintsDecorators(ranges, result);
  151. this._cache.set(model, Array.from(this._decorationsMetadata.values()).map(obj => obj.hint));
  152. }), this._getInlayHintsDelays.get(model));
  153. this._sessionDisposables.add(scheduler);
  154. // update inline hints when content or scroll position changes
  155. this._sessionDisposables.add(this._editor.onDidChangeModelContent(() => scheduler.schedule()));
  156. this._sessionDisposables.add(this._editor.onDidScrollChange(() => scheduler.schedule()));
  157. scheduler.schedule();
  158. // update inline hints when any any provider fires an event
  159. const providerListener = new DisposableStore();
  160. this._sessionDisposables.add(providerListener);
  161. for (const provider of InlayHintsProviderRegistry.all(model)) {
  162. if (typeof provider.onDidChangeInlayHints === 'function') {
  163. providerListener.add(provider.onDidChangeInlayHints(() => {
  164. if (!requests.has(model, provider)) {
  165. scheduler.schedule();
  166. }
  167. }));
  168. }
  169. }
  170. }
  171. _getHintsRanges() {
  172. const extra = 30;
  173. const model = this._editor.getModel();
  174. const visibleRanges = this._editor.getVisibleRangesPlusViewportAboveBelow();
  175. const result = [];
  176. for (const range of visibleRanges.sort(Range.compareRangesUsingStarts)) {
  177. const extendedRange = model.validateRange(new Range(range.startLineNumber - extra, range.startColumn, range.endLineNumber + extra, range.endColumn));
  178. if (result.length === 0 || !Range.areIntersectingOrTouching(result[result.length - 1], extendedRange)) {
  179. result.push(extendedRange);
  180. }
  181. else {
  182. result[result.length - 1] = Range.plusRange(result[result.length - 1], extendedRange);
  183. }
  184. }
  185. return result;
  186. }
  187. _updateHintsDecorators(ranges, hints) {
  188. const { fontSize, fontFamily } = this._getLayoutInfo();
  189. const model = this._editor.getModel();
  190. const newDecorationsData = [];
  191. const fontFamilyVar = '--code-editorInlayHintsFontFamily';
  192. this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily);
  193. for (const hint of hints) {
  194. const { text, position, whitespaceBefore, whitespaceAfter } = hint;
  195. const marginBefore = whitespaceBefore ? (fontSize / 3) | 0 : 0;
  196. const marginAfter = whitespaceAfter ? (fontSize / 3) | 0 : 0;
  197. let backgroundColor;
  198. let color;
  199. if (hint.kind === InlayHintKind.Parameter) {
  200. backgroundColor = themeColorFromId(editorInlayHintParameterBackground);
  201. color = themeColorFromId(editorInlayHintParameterForeground);
  202. }
  203. else if (hint.kind === InlayHintKind.Type) {
  204. backgroundColor = themeColorFromId(editorInlayHintTypeBackground);
  205. color = themeColorFromId(editorInlayHintTypeForeground);
  206. }
  207. else {
  208. backgroundColor = themeColorFromId(editorInlayHintBackground);
  209. color = themeColorFromId(editorInlayHintForeground);
  210. }
  211. const classNameRef = this._ruleFactory.createClassNameRef({
  212. fontSize: `${fontSize}px`,
  213. margin: `0px ${marginAfter}px 0px ${marginBefore}px`,
  214. fontFamily: `var(${fontFamilyVar}), ${EDITOR_FONT_DEFAULTS.fontFamily}`,
  215. padding: `1px ${Math.max(1, fontSize / 4) | 0}px`,
  216. borderRadius: `${(fontSize / 4) | 0}px`,
  217. verticalAlign: 'middle',
  218. backgroundColor,
  219. color
  220. });
  221. let direction = 'before';
  222. let range = Range.fromPositions(position);
  223. let word = model.getWordAtPosition(position);
  224. let usesWordRange = false;
  225. if (word) {
  226. if (word.endColumn === position.column) {
  227. direction = 'after';
  228. usesWordRange = true;
  229. range = wordToRange(word, position.lineNumber);
  230. }
  231. else if (word.startColumn === position.column) {
  232. usesWordRange = true;
  233. range = wordToRange(word, position.lineNumber);
  234. }
  235. }
  236. newDecorationsData.push({
  237. decoration: {
  238. range,
  239. options: {
  240. [direction]: {
  241. content: fixSpace(text),
  242. inlineClassNameAffectsLetterSpacing: true,
  243. inlineClassName: classNameRef.className,
  244. },
  245. description: 'InlayHint',
  246. showIfCollapsed: !usesWordRange,
  247. stickiness: 0 /* AlwaysGrowsWhenTypingAtEdges */
  248. }
  249. },
  250. classNameRef
  251. });
  252. if (newDecorationsData.length > MAX_DECORATORS) {
  253. break;
  254. }
  255. }
  256. // collect all decoration ids that are affected by the ranges
  257. // and only update those decorations
  258. const decorationIdsToReplace = [];
  259. for (const range of ranges) {
  260. for (const { id } of model.getDecorationsInRange(range, this._decorationOwnerId, true)) {
  261. const metadata = this._decorationsMetadata.get(id);
  262. if (metadata) {
  263. decorationIdsToReplace.push(id);
  264. metadata.classNameRef.dispose();
  265. this._decorationsMetadata.delete(id);
  266. }
  267. }
  268. }
  269. const newDecorationIds = model.deltaDecorations(decorationIdsToReplace, newDecorationsData.map(d => d.decoration), this._decorationOwnerId);
  270. for (let i = 0; i < newDecorationIds.length; i++) {
  271. this._decorationsMetadata.set(newDecorationIds[i], { hint: hints[i], classNameRef: newDecorationsData[i].classNameRef });
  272. }
  273. }
  274. _getLayoutInfo() {
  275. const options = this._editor.getOption(126 /* inlayHints */);
  276. const editorFontSize = this._editor.getOption(45 /* fontSize */);
  277. let fontSize = options.fontSize;
  278. if (!fontSize || fontSize < 5 || fontSize > editorFontSize) {
  279. fontSize = (editorFontSize * .9) | 0;
  280. }
  281. const fontFamily = options.fontFamily || this._editor.getOption(42 /* fontFamily */);
  282. return { fontSize, fontFamily };
  283. }
  284. _removeAllDecorations() {
  285. this._editor.deltaDecorations(Array.from(this._decorationsMetadata.keys()), []);
  286. for (let obj of this._decorationsMetadata.values()) {
  287. obj.classNameRef.dispose();
  288. }
  289. this._decorationsMetadata.clear();
  290. }
  291. }
  292. InlayHintsController.ID = 'editor.contrib.InlayHints';
  293. InlayHintsController._decorationOwnerIdPool = 0;
  294. function wordToRange(word, lineNumber) {
  295. return new Range(lineNumber, word.startColumn, lineNumber, word.endColumn);
  296. }
  297. // Prevents the view from potentially visible whitespace
  298. function fixSpace(str) {
  299. const noBreakWhitespace = '\xa0';
  300. return str.replace(/[ \t]/g, noBreakWhitespace);
  301. }
  302. registerEditorContribution(InlayHintsController.ID, InlayHintsController);
  303. CommandsRegistry.registerCommand('_executeInlayHintProvider', (accessor, ...args) => __awaiter(void 0, void 0, void 0, function* () {
  304. const [uri, range] = args;
  305. assertType(URI.isUri(uri));
  306. assertType(Range.isIRange(range));
  307. const ref = yield accessor.get(ITextModelService).createModelReference(uri);
  308. try {
  309. const data = yield getInlayHints(ref.object.textEditorModel, [Range.lift(range)], new RequestMap(), CancellationToken.None);
  310. return data;
  311. }
  312. finally {
  313. ref.dispose();
  314. }
  315. }));