gotoSymbolQuickAccess.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  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 { DeferredPromise } from '../../../base/common/async.js';
  15. import { CancellationTokenSource } from '../../../base/common/cancellation.js';
  16. import { Codicon } from '../../../base/common/codicons.js';
  17. import { pieceToQuery, prepareQuery, scoreFuzzy2 } from '../../../base/common/fuzzyScorer.js';
  18. import { Disposable, DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
  19. import { format, trim } from '../../../base/common/strings.js';
  20. import { Range } from '../../common/core/range.js';
  21. import { DocumentSymbolProviderRegistry, SymbolKinds } from '../../common/modes.js';
  22. import { OutlineModel } from '../documentSymbols/outlineModel.js';
  23. import { AbstractEditorNavigationQuickAccessProvider } from './editorNavigationQuickAccess.js';
  24. import { localize } from '../../../nls.js';
  25. export class AbstractGotoSymbolQuickAccessProvider extends AbstractEditorNavigationQuickAccessProvider {
  26. constructor(options = Object.create(null)) {
  27. super(options);
  28. this.options = options;
  29. this.options.canAcceptInBackground = true;
  30. }
  31. provideWithoutTextEditor(picker) {
  32. this.provideLabelPick(picker, localize('cannotRunGotoSymbolWithoutEditor', "To go to a symbol, first open a text editor with symbol information."));
  33. return Disposable.None;
  34. }
  35. provideWithTextEditor(context, picker, token) {
  36. const editor = context.editor;
  37. const model = this.getModel(editor);
  38. if (!model) {
  39. return Disposable.None;
  40. }
  41. // Provide symbols from model if available in registry
  42. if (DocumentSymbolProviderRegistry.has(model)) {
  43. return this.doProvideWithEditorSymbols(context, model, picker, token);
  44. }
  45. // Otherwise show an entry for a model without registry
  46. // But give a chance to resolve the symbols at a later
  47. // point if possible
  48. return this.doProvideWithoutEditorSymbols(context, model, picker, token);
  49. }
  50. doProvideWithoutEditorSymbols(context, model, picker, token) {
  51. const disposables = new DisposableStore();
  52. // Generic pick for not having any symbol information
  53. this.provideLabelPick(picker, localize('cannotRunGotoSymbolWithoutSymbolProvider', "The active text editor does not provide symbol information."));
  54. // Wait for changes to the registry and see if eventually
  55. // we do get symbols. This can happen if the picker is opened
  56. // very early after the model has loaded but before the
  57. // language registry is ready.
  58. // https://github.com/microsoft/vscode/issues/70607
  59. (() => __awaiter(this, void 0, void 0, function* () {
  60. const result = yield this.waitForLanguageSymbolRegistry(model, disposables);
  61. if (!result || token.isCancellationRequested) {
  62. return;
  63. }
  64. disposables.add(this.doProvideWithEditorSymbols(context, model, picker, token));
  65. }))();
  66. return disposables;
  67. }
  68. provideLabelPick(picker, label) {
  69. picker.items = [{ label, index: 0, kind: 14 /* String */ }];
  70. picker.ariaLabel = label;
  71. }
  72. waitForLanguageSymbolRegistry(model, disposables) {
  73. return __awaiter(this, void 0, void 0, function* () {
  74. if (DocumentSymbolProviderRegistry.has(model)) {
  75. return true;
  76. }
  77. const symbolProviderRegistryPromise = new DeferredPromise();
  78. // Resolve promise when registry knows model
  79. const symbolProviderListener = disposables.add(DocumentSymbolProviderRegistry.onDidChange(() => {
  80. if (DocumentSymbolProviderRegistry.has(model)) {
  81. symbolProviderListener.dispose();
  82. symbolProviderRegistryPromise.complete(true);
  83. }
  84. }));
  85. // Resolve promise when we get disposed too
  86. disposables.add(toDisposable(() => symbolProviderRegistryPromise.complete(false)));
  87. return symbolProviderRegistryPromise.p;
  88. });
  89. }
  90. doProvideWithEditorSymbols(context, model, picker, token) {
  91. const editor = context.editor;
  92. const disposables = new DisposableStore();
  93. // Goto symbol once picked
  94. disposables.add(picker.onDidAccept(event => {
  95. const [item] = picker.selectedItems;
  96. if (item && item.range) {
  97. this.gotoLocation(context, { range: item.range.selection, keyMods: picker.keyMods, preserveFocus: event.inBackground });
  98. if (!event.inBackground) {
  99. picker.hide();
  100. }
  101. }
  102. }));
  103. // Goto symbol side by side if enabled
  104. disposables.add(picker.onDidTriggerItemButton(({ item }) => {
  105. if (item && item.range) {
  106. this.gotoLocation(context, { range: item.range.selection, keyMods: picker.keyMods, forceSideBySide: true });
  107. picker.hide();
  108. }
  109. }));
  110. // Resolve symbols from document once and reuse this
  111. // request for all filtering and typing then on
  112. const symbolsPromise = this.getDocumentSymbols(model, token);
  113. // Set initial picks and update on type
  114. let picksCts = undefined;
  115. const updatePickerItems = () => __awaiter(this, void 0, void 0, function* () {
  116. // Cancel any previous ask for picks and busy
  117. picksCts === null || picksCts === void 0 ? void 0 : picksCts.dispose(true);
  118. picker.busy = false;
  119. // Create new cancellation source for this run
  120. picksCts = new CancellationTokenSource(token);
  121. // Collect symbol picks
  122. picker.busy = true;
  123. try {
  124. const query = prepareQuery(picker.value.substr(AbstractGotoSymbolQuickAccessProvider.PREFIX.length).trim());
  125. const items = yield this.doGetSymbolPicks(symbolsPromise, query, undefined, picksCts.token);
  126. if (token.isCancellationRequested) {
  127. return;
  128. }
  129. if (items.length > 0) {
  130. picker.items = items;
  131. }
  132. else {
  133. if (query.original.length > 0) {
  134. this.provideLabelPick(picker, localize('noMatchingSymbolResults', "No matching editor symbols"));
  135. }
  136. else {
  137. this.provideLabelPick(picker, localize('noSymbolResults', "No editor symbols"));
  138. }
  139. }
  140. }
  141. finally {
  142. if (!token.isCancellationRequested) {
  143. picker.busy = false;
  144. }
  145. }
  146. });
  147. disposables.add(picker.onDidChangeValue(() => updatePickerItems()));
  148. updatePickerItems();
  149. // Reveal and decorate when active item changes
  150. // However, ignore the very first event so that
  151. // opening the picker is not immediately revealing
  152. // and decorating the first entry.
  153. let ignoreFirstActiveEvent = true;
  154. disposables.add(picker.onDidChangeActive(() => {
  155. const [item] = picker.activeItems;
  156. if (item && item.range) {
  157. if (ignoreFirstActiveEvent) {
  158. ignoreFirstActiveEvent = false;
  159. return;
  160. }
  161. // Reveal
  162. editor.revealRangeInCenter(item.range.selection, 0 /* Smooth */);
  163. // Decorate
  164. this.addDecorations(editor, item.range.decoration);
  165. }
  166. }));
  167. return disposables;
  168. }
  169. doGetSymbolPicks(symbolsPromise, query, options, token) {
  170. return __awaiter(this, void 0, void 0, function* () {
  171. const symbols = yield symbolsPromise;
  172. if (token.isCancellationRequested) {
  173. return [];
  174. }
  175. const filterBySymbolKind = query.original.indexOf(AbstractGotoSymbolQuickAccessProvider.SCOPE_PREFIX) === 0;
  176. const filterPos = filterBySymbolKind ? 1 : 0;
  177. // Split between symbol and container query
  178. let symbolQuery;
  179. let containerQuery;
  180. if (query.values && query.values.length > 1) {
  181. symbolQuery = pieceToQuery(query.values[0]); // symbol: only match on first part
  182. containerQuery = pieceToQuery(query.values.slice(1)); // container: match on all but first parts
  183. }
  184. else {
  185. symbolQuery = query;
  186. }
  187. // Convert to symbol picks and apply filtering
  188. const filteredSymbolPicks = [];
  189. for (let index = 0; index < symbols.length; index++) {
  190. const symbol = symbols[index];
  191. const symbolLabel = trim(symbol.name);
  192. const symbolLabelWithIcon = `$(symbol-${SymbolKinds.toString(symbol.kind) || 'property'}) ${symbolLabel}`;
  193. const symbolLabelIconOffset = symbolLabelWithIcon.length - symbolLabel.length;
  194. let containerLabel = symbol.containerName;
  195. if (options === null || options === void 0 ? void 0 : options.extraContainerLabel) {
  196. if (containerLabel) {
  197. containerLabel = `${options.extraContainerLabel} • ${containerLabel}`;
  198. }
  199. else {
  200. containerLabel = options.extraContainerLabel;
  201. }
  202. }
  203. let symbolScore = undefined;
  204. let symbolMatches = undefined;
  205. let containerScore = undefined;
  206. let containerMatches = undefined;
  207. if (query.original.length > filterPos) {
  208. // First: try to score on the entire query, it is possible that
  209. // the symbol matches perfectly (e.g. searching for "change log"
  210. // can be a match on a markdown symbol "change log"). In that
  211. // case we want to skip the container query altogether.
  212. let skipContainerQuery = false;
  213. if (symbolQuery !== query) {
  214. [symbolScore, symbolMatches] = scoreFuzzy2(symbolLabelWithIcon, Object.assign(Object.assign({}, query), { values: undefined /* disable multi-query support */ }), filterPos, symbolLabelIconOffset);
  215. if (typeof symbolScore === 'number') {
  216. skipContainerQuery = true; // since we consumed the query, skip any container matching
  217. }
  218. }
  219. // Otherwise: score on the symbol query and match on the container later
  220. if (typeof symbolScore !== 'number') {
  221. [symbolScore, symbolMatches] = scoreFuzzy2(symbolLabelWithIcon, symbolQuery, filterPos, symbolLabelIconOffset);
  222. if (typeof symbolScore !== 'number') {
  223. continue;
  224. }
  225. }
  226. // Score by container if specified
  227. if (!skipContainerQuery && containerQuery) {
  228. if (containerLabel && containerQuery.original.length > 0) {
  229. [containerScore, containerMatches] = scoreFuzzy2(containerLabel, containerQuery);
  230. }
  231. if (typeof containerScore !== 'number') {
  232. continue;
  233. }
  234. if (typeof symbolScore === 'number') {
  235. symbolScore += containerScore; // boost symbolScore by containerScore
  236. }
  237. }
  238. }
  239. const deprecated = symbol.tags && symbol.tags.indexOf(1 /* Deprecated */) >= 0;
  240. filteredSymbolPicks.push({
  241. index,
  242. kind: symbol.kind,
  243. score: symbolScore,
  244. label: symbolLabelWithIcon,
  245. ariaLabel: symbolLabel,
  246. description: containerLabel,
  247. highlights: deprecated ? undefined : {
  248. label: symbolMatches,
  249. description: containerMatches
  250. },
  251. range: {
  252. selection: Range.collapseToStart(symbol.selectionRange),
  253. decoration: symbol.range
  254. },
  255. strikethrough: deprecated,
  256. buttons: (() => {
  257. var _a, _b;
  258. const openSideBySideDirection = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.openSideBySideDirection) ? (_b = this.options) === null || _b === void 0 ? void 0 : _b.openSideBySideDirection() : undefined;
  259. if (!openSideBySideDirection) {
  260. return undefined;
  261. }
  262. return [
  263. {
  264. iconClass: openSideBySideDirection === 'right' ? Codicon.splitHorizontal.classNames : Codicon.splitVertical.classNames,
  265. tooltip: openSideBySideDirection === 'right' ? localize('openToSide', "Open to the Side") : localize('openToBottom', "Open to the Bottom")
  266. }
  267. ];
  268. })()
  269. });
  270. }
  271. // Sort by score
  272. const sortedFilteredSymbolPicks = filteredSymbolPicks.sort((symbolA, symbolB) => filterBySymbolKind ?
  273. this.compareByKindAndScore(symbolA, symbolB) :
  274. this.compareByScore(symbolA, symbolB));
  275. // Add separator for types
  276. // - @ only total number of symbols
  277. // - @: grouped by symbol kind
  278. let symbolPicks = [];
  279. if (filterBySymbolKind) {
  280. let lastSymbolKind = undefined;
  281. let lastSeparator = undefined;
  282. let lastSymbolKindCounter = 0;
  283. function updateLastSeparatorLabel() {
  284. if (lastSeparator && typeof lastSymbolKind === 'number' && lastSymbolKindCounter > 0) {
  285. lastSeparator.label = format(NLS_SYMBOL_KIND_CACHE[lastSymbolKind] || FALLBACK_NLS_SYMBOL_KIND, lastSymbolKindCounter);
  286. }
  287. }
  288. for (const symbolPick of sortedFilteredSymbolPicks) {
  289. // Found new kind
  290. if (lastSymbolKind !== symbolPick.kind) {
  291. // Update last separator with number of symbols we found for kind
  292. updateLastSeparatorLabel();
  293. lastSymbolKind = symbolPick.kind;
  294. lastSymbolKindCounter = 1;
  295. // Add new separator for new kind
  296. lastSeparator = { type: 'separator' };
  297. symbolPicks.push(lastSeparator);
  298. }
  299. // Existing kind, keep counting
  300. else {
  301. lastSymbolKindCounter++;
  302. }
  303. // Add to final result
  304. symbolPicks.push(symbolPick);
  305. }
  306. // Update last separator with number of symbols we found for kind
  307. updateLastSeparatorLabel();
  308. }
  309. else if (sortedFilteredSymbolPicks.length > 0) {
  310. symbolPicks = [
  311. { label: localize('symbols', "symbols ({0})", filteredSymbolPicks.length), type: 'separator' },
  312. ...sortedFilteredSymbolPicks
  313. ];
  314. }
  315. return symbolPicks;
  316. });
  317. }
  318. compareByScore(symbolA, symbolB) {
  319. if (typeof symbolA.score !== 'number' && typeof symbolB.score === 'number') {
  320. return 1;
  321. }
  322. else if (typeof symbolA.score === 'number' && typeof symbolB.score !== 'number') {
  323. return -1;
  324. }
  325. if (typeof symbolA.score === 'number' && typeof symbolB.score === 'number') {
  326. if (symbolA.score > symbolB.score) {
  327. return -1;
  328. }
  329. else if (symbolA.score < symbolB.score) {
  330. return 1;
  331. }
  332. }
  333. if (symbolA.index < symbolB.index) {
  334. return -1;
  335. }
  336. else if (symbolA.index > symbolB.index) {
  337. return 1;
  338. }
  339. return 0;
  340. }
  341. compareByKindAndScore(symbolA, symbolB) {
  342. const kindA = NLS_SYMBOL_KIND_CACHE[symbolA.kind] || FALLBACK_NLS_SYMBOL_KIND;
  343. const kindB = NLS_SYMBOL_KIND_CACHE[symbolB.kind] || FALLBACK_NLS_SYMBOL_KIND;
  344. // Sort by type first if scoped search
  345. const result = kindA.localeCompare(kindB);
  346. if (result === 0) {
  347. return this.compareByScore(symbolA, symbolB);
  348. }
  349. return result;
  350. }
  351. getDocumentSymbols(document, token) {
  352. return __awaiter(this, void 0, void 0, function* () {
  353. const model = yield OutlineModel.create(document, token);
  354. return token.isCancellationRequested ? [] : model.asListOfDocumentSymbols();
  355. });
  356. }
  357. }
  358. AbstractGotoSymbolQuickAccessProvider.PREFIX = '@';
  359. AbstractGotoSymbolQuickAccessProvider.SCOPE_PREFIX = ':';
  360. AbstractGotoSymbolQuickAccessProvider.PREFIX_BY_CATEGORY = `${AbstractGotoSymbolQuickAccessProvider.PREFIX}${AbstractGotoSymbolQuickAccessProvider.SCOPE_PREFIX}`;
  361. // #region NLS Helpers
  362. const FALLBACK_NLS_SYMBOL_KIND = localize('property', "properties ({0})");
  363. const NLS_SYMBOL_KIND_CACHE = {
  364. [5 /* Method */]: localize('method', "methods ({0})"),
  365. [11 /* Function */]: localize('function', "functions ({0})"),
  366. [8 /* Constructor */]: localize('_constructor', "constructors ({0})"),
  367. [12 /* Variable */]: localize('variable', "variables ({0})"),
  368. [4 /* Class */]: localize('class', "classes ({0})"),
  369. [22 /* Struct */]: localize('struct', "structs ({0})"),
  370. [23 /* Event */]: localize('event', "events ({0})"),
  371. [24 /* Operator */]: localize('operator', "operators ({0})"),
  372. [10 /* Interface */]: localize('interface', "interfaces ({0})"),
  373. [2 /* Namespace */]: localize('namespace', "namespaces ({0})"),
  374. [3 /* Package */]: localize('package', "packages ({0})"),
  375. [25 /* TypeParameter */]: localize('typeParameter', "type parameters ({0})"),
  376. [1 /* Module */]: localize('modules', "modules ({0})"),
  377. [6 /* Property */]: localize('property', "properties ({0})"),
  378. [9 /* Enum */]: localize('enum', "enumerations ({0})"),
  379. [21 /* EnumMember */]: localize('enumMember', "enumeration members ({0})"),
  380. [14 /* String */]: localize('string', "strings ({0})"),
  381. [0 /* File */]: localize('file', "files ({0})"),
  382. [17 /* Array */]: localize('array', "arrays ({0})"),
  383. [15 /* Number */]: localize('number', "numbers ({0})"),
  384. [16 /* Boolean */]: localize('boolean', "booleans ({0})"),
  385. [18 /* Object */]: localize('object', "objects ({0})"),
  386. [19 /* Key */]: localize('key', "keys ({0})"),
  387. [7 /* Field */]: localize('field', "fields ({0})"),
  388. [13 /* Constant */]: localize('constant', "constants ({0})")
  389. };
  390. //#endregion