outlineModel.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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 { equals } from '../../../base/common/arrays.js';
  6. import { CancellationTokenSource } from '../../../base/common/cancellation.js';
  7. import { onUnexpectedExternalError } from '../../../base/common/errors.js';
  8. import { Iterable } from '../../../base/common/iterator.js';
  9. import { LRUCache } from '../../../base/common/map.js';
  10. import { Range } from '../../common/core/range.js';
  11. import { DocumentSymbolProviderRegistry } from '../../common/modes.js';
  12. import { LanguageFeatureRequestDelays } from '../../common/modes/languageFeatureRegistry.js';
  13. export class TreeElement {
  14. remove() {
  15. if (this.parent) {
  16. this.parent.children.delete(this.id);
  17. }
  18. }
  19. static findId(candidate, container) {
  20. // complex id-computation which contains the origin/extension,
  21. // the parent path, and some dedupe logic when names collide
  22. let candidateId;
  23. if (typeof candidate === 'string') {
  24. candidateId = `${container.id}/${candidate}`;
  25. }
  26. else {
  27. candidateId = `${container.id}/${candidate.name}`;
  28. if (container.children.get(candidateId) !== undefined) {
  29. candidateId = `${container.id}/${candidate.name}_${candidate.range.startLineNumber}_${candidate.range.startColumn}`;
  30. }
  31. }
  32. let id = candidateId;
  33. for (let i = 0; container.children.get(id) !== undefined; i++) {
  34. id = `${candidateId}_${i}`;
  35. }
  36. return id;
  37. }
  38. static empty(element) {
  39. return element.children.size === 0;
  40. }
  41. }
  42. export class OutlineElement extends TreeElement {
  43. constructor(id, parent, symbol) {
  44. super();
  45. this.id = id;
  46. this.parent = parent;
  47. this.symbol = symbol;
  48. this.children = new Map();
  49. }
  50. }
  51. export class OutlineGroup extends TreeElement {
  52. constructor(id, parent, label, order) {
  53. super();
  54. this.id = id;
  55. this.parent = parent;
  56. this.label = label;
  57. this.order = order;
  58. this.children = new Map();
  59. }
  60. }
  61. export class OutlineModel extends TreeElement {
  62. constructor(uri) {
  63. super();
  64. this.uri = uri;
  65. this.id = 'root';
  66. this.parent = undefined;
  67. this._groups = new Map();
  68. this.children = new Map();
  69. this.id = 'root';
  70. this.parent = undefined;
  71. }
  72. static create(textModel, token) {
  73. let key = this._keys.for(textModel, true);
  74. let data = OutlineModel._requests.get(key);
  75. if (!data) {
  76. let source = new CancellationTokenSource();
  77. data = {
  78. promiseCnt: 0,
  79. source,
  80. promise: OutlineModel._create(textModel, source.token),
  81. model: undefined,
  82. };
  83. OutlineModel._requests.set(key, data);
  84. // keep moving average of request durations
  85. const now = Date.now();
  86. data.promise.then(() => {
  87. this._requestDurations.update(textModel, Date.now() - now);
  88. });
  89. }
  90. if (data.model) {
  91. // resolved -> return data
  92. return Promise.resolve(data.model);
  93. }
  94. // increase usage counter
  95. data.promiseCnt += 1;
  96. token.onCancellationRequested(() => {
  97. // last -> cancel provider request, remove cached promise
  98. if (--data.promiseCnt === 0) {
  99. data.source.cancel();
  100. OutlineModel._requests.delete(key);
  101. }
  102. });
  103. return new Promise((resolve, reject) => {
  104. data.promise.then(model => {
  105. data.model = model;
  106. resolve(model);
  107. }, err => {
  108. OutlineModel._requests.delete(key);
  109. reject(err);
  110. });
  111. });
  112. }
  113. static _create(textModel, token) {
  114. const cts = new CancellationTokenSource(token);
  115. const result = new OutlineModel(textModel.uri);
  116. const provider = DocumentSymbolProviderRegistry.ordered(textModel);
  117. const promises = provider.map((provider, index) => {
  118. var _a;
  119. let id = TreeElement.findId(`provider_${index}`, result);
  120. let group = new OutlineGroup(id, result, (_a = provider.displayName) !== null && _a !== void 0 ? _a : 'Unknown Outline Provider', index);
  121. return Promise.resolve(provider.provideDocumentSymbols(textModel, cts.token)).then(result => {
  122. for (const info of result || []) {
  123. OutlineModel._makeOutlineElement(info, group);
  124. }
  125. return group;
  126. }, err => {
  127. onUnexpectedExternalError(err);
  128. return group;
  129. }).then(group => {
  130. if (!TreeElement.empty(group)) {
  131. result._groups.set(id, group);
  132. }
  133. else {
  134. group.remove();
  135. }
  136. });
  137. });
  138. const listener = DocumentSymbolProviderRegistry.onDidChange(() => {
  139. const newProvider = DocumentSymbolProviderRegistry.ordered(textModel);
  140. if (!equals(newProvider, provider)) {
  141. cts.cancel();
  142. }
  143. });
  144. return Promise.all(promises).then(() => {
  145. if (cts.token.isCancellationRequested && !token.isCancellationRequested) {
  146. return OutlineModel._create(textModel, token);
  147. }
  148. else {
  149. return result._compact();
  150. }
  151. }).finally(() => {
  152. listener.dispose();
  153. });
  154. }
  155. static _makeOutlineElement(info, container) {
  156. let id = TreeElement.findId(info, container);
  157. let res = new OutlineElement(id, container, info);
  158. if (info.children) {
  159. for (const childInfo of info.children) {
  160. OutlineModel._makeOutlineElement(childInfo, res);
  161. }
  162. }
  163. container.children.set(res.id, res);
  164. }
  165. _compact() {
  166. let count = 0;
  167. for (const [key, group] of this._groups) {
  168. if (group.children.size === 0) { // empty
  169. this._groups.delete(key);
  170. }
  171. else {
  172. count += 1;
  173. }
  174. }
  175. if (count !== 1) {
  176. //
  177. this.children = this._groups;
  178. }
  179. else {
  180. // adopt all elements of the first group
  181. let group = Iterable.first(this._groups.values());
  182. for (let [, child] of group.children) {
  183. child.parent = this;
  184. this.children.set(child.id, child);
  185. }
  186. }
  187. return this;
  188. }
  189. getTopLevelSymbols() {
  190. const roots = [];
  191. for (const child of this.children.values()) {
  192. if (child instanceof OutlineElement) {
  193. roots.push(child.symbol);
  194. }
  195. else {
  196. roots.push(...Iterable.map(child.children.values(), child => child.symbol));
  197. }
  198. }
  199. return roots.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range));
  200. }
  201. asListOfDocumentSymbols() {
  202. const roots = this.getTopLevelSymbols();
  203. const bucket = [];
  204. OutlineModel._flattenDocumentSymbols(bucket, roots, '');
  205. return bucket.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range));
  206. }
  207. static _flattenDocumentSymbols(bucket, entries, overrideContainerLabel) {
  208. for (const entry of entries) {
  209. bucket.push({
  210. kind: entry.kind,
  211. tags: entry.tags,
  212. name: entry.name,
  213. detail: entry.detail,
  214. containerName: entry.containerName || overrideContainerLabel,
  215. range: entry.range,
  216. selectionRange: entry.selectionRange,
  217. children: undefined, // we flatten it...
  218. });
  219. // Recurse over children
  220. if (entry.children) {
  221. OutlineModel._flattenDocumentSymbols(bucket, entry.children, entry.name);
  222. }
  223. }
  224. }
  225. }
  226. OutlineModel._requestDurations = new LanguageFeatureRequestDelays(DocumentSymbolProviderRegistry, 350);
  227. OutlineModel._requests = new LRUCache(9, 0.75);
  228. OutlineModel._keys = new class {
  229. constructor() {
  230. this._counter = 1;
  231. this._data = new WeakMap();
  232. }
  233. for(textModel, version) {
  234. return `${textModel.id}/${version ? textModel.getVersionId() : ''}/${this._hash(DocumentSymbolProviderRegistry.all(textModel))}`;
  235. }
  236. _hash(providers) {
  237. let result = '';
  238. for (const provider of providers) {
  239. let n = this._data.get(provider);
  240. if (typeof n === 'undefined') {
  241. n = this._counter++;
  242. this._data.set(provider, n);
  243. }
  244. result += n;
  245. }
  246. return result;
  247. }
  248. };