completionModel.js 9.5 KB

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