ecfc5206477a6d52dc448d9ddbc5b811b911705c1291211aaf12d4143f77be69aaa01ec79ce24ebe87bd55bfdf771df600219d95e96abceba720fd5a0a565c 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  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 { quickSelect } from '../../../../base/common/arrays.js';
  6. import { anyScore, fuzzyScore, FuzzyScore, fuzzyScoreGracefulAggressive, FuzzyScoreOptions } from '../../../../base/common/filters.js';
  7. import { compareIgnoreCase } from '../../../../base/common/strings.js';
  8. export class LineContext {
  9. constructor(leadingLineContent, characterCountDelta) {
  10. this.leadingLineContent = leadingLineContent;
  11. this.characterCountDelta = characterCountDelta;
  12. }
  13. }
  14. /**
  15. * Sorted, filtered completion view model
  16. * */
  17. export class CompletionModel {
  18. constructor(items, column, lineContext, wordDistance, options, snippetSuggestions, fuzzyScoreOptions = FuzzyScoreOptions.default, clipboardText = undefined) {
  19. this.clipboardText = clipboardText;
  20. this._snippetCompareFn = CompletionModel._compareCompletionItems;
  21. this._items = items;
  22. this._column = column;
  23. this._wordDistance = wordDistance;
  24. this._options = options;
  25. this._refilterKind = 1 /* Refilter.All */;
  26. this._lineContext = lineContext;
  27. this._fuzzyScoreOptions = fuzzyScoreOptions;
  28. if (snippetSuggestions === 'top') {
  29. this._snippetCompareFn = CompletionModel._compareCompletionItemsSnippetsUp;
  30. }
  31. else if (snippetSuggestions === 'bottom') {
  32. this._snippetCompareFn = CompletionModel._compareCompletionItemsSnippetsDown;
  33. }
  34. }
  35. get lineContext() {
  36. return this._lineContext;
  37. }
  38. set lineContext(value) {
  39. if (this._lineContext.leadingLineContent !== value.leadingLineContent
  40. || this._lineContext.characterCountDelta !== value.characterCountDelta) {
  41. this._refilterKind = this._lineContext.characterCountDelta < value.characterCountDelta && this._filteredItems ? 2 /* Refilter.Incr */ : 1 /* Refilter.All */;
  42. this._lineContext = value;
  43. }
  44. }
  45. get items() {
  46. this._ensureCachedState();
  47. return this._filteredItems;
  48. }
  49. get allProvider() {
  50. this._ensureCachedState();
  51. return this._providerInfo.keys();
  52. }
  53. get incomplete() {
  54. this._ensureCachedState();
  55. const result = new Set();
  56. for (const [provider, incomplete] of this._providerInfo) {
  57. if (incomplete) {
  58. result.add(provider);
  59. }
  60. }
  61. return result;
  62. }
  63. adopt(except) {
  64. const res = [];
  65. for (let i = 0; i < this._items.length;) {
  66. if (!except.has(this._items[i].provider)) {
  67. res.push(this._items[i]);
  68. // unordered removed
  69. this._items[i] = this._items[this._items.length - 1];
  70. this._items.pop();
  71. }
  72. else {
  73. // continue with next item
  74. i++;
  75. }
  76. }
  77. this._refilterKind = 1 /* Refilter.All */;
  78. return res;
  79. }
  80. get stats() {
  81. this._ensureCachedState();
  82. return this._stats;
  83. }
  84. _ensureCachedState() {
  85. if (this._refilterKind !== 0 /* Refilter.Nothing */) {
  86. this._createCachedState();
  87. }
  88. }
  89. _createCachedState() {
  90. this._providerInfo = new Map();
  91. const labelLengths = [];
  92. const { leadingLineContent, characterCountDelta } = this._lineContext;
  93. let word = '';
  94. let wordLow = '';
  95. // incrementally filter less
  96. const source = this._refilterKind === 1 /* Refilter.All */ ? this._items : this._filteredItems;
  97. const target = [];
  98. // picks a score function based on the number of
  99. // items that we have to score/filter and based on the
  100. // user-configuration
  101. const scoreFn = (!this._options.filterGraceful || source.length > 2000) ? fuzzyScore : fuzzyScoreGracefulAggressive;
  102. for (let i = 0; i < source.length; i++) {
  103. const item = source[i];
  104. if (item.isInvalid) {
  105. continue; // SKIP invalid items
  106. }
  107. // collect all support, know if their result is incomplete
  108. this._providerInfo.set(item.provider, Boolean(item.container.incomplete));
  109. // 'word' is that remainder of the current line that we
  110. // filter and score against. In theory each suggestion uses a
  111. // different word, but in practice not - that's why we cache
  112. const overwriteBefore = item.position.column - item.editStart.column;
  113. const wordLen = overwriteBefore + characterCountDelta - (item.position.column - this._column);
  114. if (word.length !== wordLen) {
  115. word = wordLen === 0 ? '' : leadingLineContent.slice(-wordLen);
  116. wordLow = word.toLowerCase();
  117. }
  118. // remember the word against which this item was
  119. // scored
  120. item.word = word;
  121. if (wordLen === 0) {
  122. // when there is nothing to score against, don't
  123. // event try to do. Use a const rank and rely on
  124. // the fallback-sort using the initial sort order.
  125. // use a score of `-100` because that is out of the
  126. // bound of values `fuzzyScore` will return
  127. item.score = FuzzyScore.Default;
  128. }
  129. else {
  130. // skip word characters that are whitespace until
  131. // we have hit the replace range (overwriteBefore)
  132. let wordPos = 0;
  133. while (wordPos < overwriteBefore) {
  134. const ch = word.charCodeAt(wordPos);
  135. if (ch === 32 /* CharCode.Space */ || ch === 9 /* CharCode.Tab */) {
  136. wordPos += 1;
  137. }
  138. else {
  139. break;
  140. }
  141. }
  142. if (wordPos >= wordLen) {
  143. // the wordPos at which scoring starts is the whole word
  144. // and therefore the same rules as not having a word apply
  145. item.score = FuzzyScore.Default;
  146. }
  147. else if (typeof item.completion.filterText === 'string') {
  148. // when there is a `filterText` it must match the `word`.
  149. // if it matches we check with the label to compute highlights
  150. // and if that doesn't yield a result we have no highlights,
  151. // despite having the match
  152. const match = scoreFn(word, wordLow, wordPos, item.completion.filterText, item.filterTextLow, 0, this._fuzzyScoreOptions);
  153. if (!match) {
  154. continue; // NO match
  155. }
  156. if (compareIgnoreCase(item.completion.filterText, item.textLabel) === 0) {
  157. // filterText and label are actually the same -> use good highlights
  158. item.score = match;
  159. }
  160. else {
  161. // re-run the scorer on the label in the hope of a result BUT use the rank
  162. // of the filterText-match
  163. item.score = anyScore(word, wordLow, wordPos, item.textLabel, item.labelLow, 0);
  164. item.score[0] = match[0]; // use score from filterText
  165. }
  166. }
  167. else {
  168. // by default match `word` against the `label`
  169. const match = scoreFn(word, wordLow, wordPos, item.textLabel, item.labelLow, 0, this._fuzzyScoreOptions);
  170. if (!match) {
  171. continue; // NO match
  172. }
  173. item.score = match;
  174. }
  175. }
  176. item.idx = i;
  177. item.distance = this._wordDistance.distance(item.position, item.completion);
  178. target.push(item);
  179. // update stats
  180. labelLengths.push(item.textLabel.length);
  181. }
  182. this._filteredItems = target.sort(this._snippetCompareFn);
  183. this._refilterKind = 0 /* Refilter.Nothing */;
  184. this._stats = {
  185. pLabelLen: labelLengths.length ?
  186. quickSelect(labelLengths.length - .85, labelLengths, (a, b) => a - b)
  187. : 0
  188. };
  189. }
  190. static _compareCompletionItems(a, b) {
  191. if (a.score[0] > b.score[0]) {
  192. return -1;
  193. }
  194. else if (a.score[0] < b.score[0]) {
  195. return 1;
  196. }
  197. else if (a.distance < b.distance) {
  198. return -1;
  199. }
  200. else if (a.distance > b.distance) {
  201. return 1;
  202. }
  203. else if (a.idx < b.idx) {
  204. return -1;
  205. }
  206. else if (a.idx > b.idx) {
  207. return 1;
  208. }
  209. else {
  210. return 0;
  211. }
  212. }
  213. static _compareCompletionItemsSnippetsDown(a, b) {
  214. if (a.completion.kind !== b.completion.kind) {
  215. if (a.completion.kind === 27 /* CompletionItemKind.Snippet */) {
  216. return 1;
  217. }
  218. else if (b.completion.kind === 27 /* CompletionItemKind.Snippet */) {
  219. return -1;
  220. }
  221. }
  222. return CompletionModel._compareCompletionItems(a, b);
  223. }
  224. static _compareCompletionItemsSnippetsUp(a, b) {
  225. if (a.completion.kind !== b.completion.kind) {
  226. if (a.completion.kind === 27 /* CompletionItemKind.Snippet */) {
  227. return -1;
  228. }
  229. else if (b.completion.kind === 27 /* CompletionItemKind.Snippet */) {
  230. return 1;
  231. }
  232. }
  233. return CompletionModel._compareCompletionItems(a, b);
  234. }
  235. }