Skip to content

Commit 559f54a

Browse files
committed
feat(suggestions): Locality bonus! (isabled by default*, SUPER recommended, enable with suggestions.localityBonus)
fixes #46
1 parent b075ff1 commit 559f54a

File tree

5 files changed

+73
-43
lines changed

5 files changed

+73
-43
lines changed

README.MD

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ const usersList = []
6767
usersList.map // -> usersList.map((user) => )
6868
```
6969

70+
### Locality Bonus
71+
72+
(*disabled by default*, SUPER recommended, enable with `suggestions.localityBonus`)
73+
74+
Suggestions closer to cursor will appear higher, useful for local variables (eg callback parameters), requires TS >=5.0
75+
76+
Why not enable built-in *Locality Bonus* setting:
77+
78+
- Sometimes it just doesn't work
79+
- In other cases it might lead to misleading suggestions (as it's text-based and not smart)
80+
7081
### Case-sensitive Completions
7182

7283
(*disabled by default*)
@@ -256,6 +267,7 @@ There are value descriptions for two settings:
256267
257268
```ts
258269
const example = ({ a }, b?, c = 5, ...d) => { }
270+
259271
// binding-name (default)
260272
example({ a }, b, c, ...d)
261273
// always-declaration (also popular)
@@ -268,11 +280,14 @@ example(__0, b, c, d)
268280
269281
```ts
270282
const example = ({ a }, b?, c = 5, ...d) => { }
283+
271284
// only-rest
272285
example({ a }, b, c)
273-
// optional-and-rest
286+
// optional-and-rest (default)
274287
example({ a })
275-
// no-skip (default)
288+
// all
289+
example() // (cursor inside)
290+
// no-skip (popular)
276291
example({ a }, b, c, ...d)
277292
```
278293

src/configurationType.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ export type Configuration = {
121121
* @default disable
122122
*/
123123
'suggestions.displayImportedInfo': 'disable' | 'short-format' | 'long-format'
124+
/**
125+
* @recommended
126+
* @default false
127+
*/
128+
'suggestions.localityBonus': boolean
124129
// TODO! corrent watching!
125130
/**
126131
* Wether to enable snippets for array methods like `items.map(item => )`
Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { PrevCompletionMap } from '../completionsAtPosition'
2-
import { GetConfig } from '../types'
31
import stringDedent from 'string-dedent'
2+
import { sharedCompletionContext } from './sharedContext'
43

5-
export default (entries: ts.CompletionEntry[], prevCompletionsMap: PrevCompletionMap, c: GetConfig): ts.CompletionEntry[] | undefined => {
6-
if (!c('displayAdditionalInfoInCompletions')) return entries
7-
return entries.map(entry => {
4+
export default (entries: ts.CompletionEntry[]) => {
5+
const { prevCompletionsMap, c } = sharedCompletionContext
6+
if (!c('displayAdditionalInfoInCompletions')) return
7+
for (const entry of entries) {
88
const symbol = entry['symbol'] as ts.Symbol | undefined
9-
if (!symbol) return entry
9+
if (!symbol) continue
1010
const addNodeText = (node: ts.Node) => {
1111
let text = node.getText().trim()
1212
if (ts.isBlock(node)) text = text.slice(1, -1)
@@ -20,9 +20,9 @@ export default (entries: ts.CompletionEntry[], prevCompletionsMap: PrevCompletio
2020
}
2121
}
2222
let node: ts.Node = symbol.valueDeclaration!
23-
if (!node) return entry
23+
if (!node) continue
2424
if (ts.isVariableDeclaration(node)) node = node.initializer!
25-
if (!node) return entry
25+
if (!node) continue
2626
if ((ts.isMethodDeclaration(node) || ts.isArrowFunction(node)) && node.body) {
2727
const { body } = node
2828
if (ts.isBlock(body) && body.statements.length === 1 && ts.isReturnStatement(body.statements[0]!)) {
@@ -31,6 +31,5 @@ export default (entries: ts.CompletionEntry[], prevCompletionsMap: PrevCompletio
3131
addNodeText(body)
3232
}
3333
}
34-
return entry
35-
})
34+
}
3635
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { sharedCompletionContext } from './sharedContext'
2+
3+
export default (entries: ts.CompletionEntry[]) => {
4+
let { node, sourceFile, c } = sharedCompletionContext
5+
if (!c('suggestions.localityBonus')) return
6+
7+
const getScore = entry => {
8+
const symbol: ts.Symbol | undefined = entry['symbol']
9+
if (!symbol) return
10+
const { valueDeclaration } = symbol
11+
if (!valueDeclaration) return
12+
if (valueDeclaration.getSourceFile().fileName !== sourceFile.fileName) return -1
13+
return valueDeclaration.pos
14+
}
15+
if (!node) return
16+
return [...entries].sort((a, b) => {
17+
const aScore = getScore(a)
18+
const bScore = getScore(b)
19+
if (aScore === undefined || bScore === undefined) return 0
20+
return bScore - aScore
21+
})
22+
}

typescript/src/completionsAtPosition.ts

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import inKeywordCompletions from './completions/inKeywordCompletions'
33
// import * as emmet from '@vscode/emmet-helper'
44
import isInBannedPosition from './completions/isInBannedPosition'
55
import { GetConfig } from './types'
6-
import { findChildContainingExactPosition, findChildContainingPosition, isTs5 } from './utils'
6+
import { findChildContainingExactPosition, findChildContainingPosition, isTs5, patchMethod } from './utils'
77
import indexSignatureAccessCompletions from './completions/indexSignatureAccess'
88
import fixPropertiesSorting from './completions/fixPropertiesSorting'
99
import { isGoodPositionBuiltinMethodCompletion } from './completions/isGoodPositionMethodCompletion'
@@ -20,7 +20,7 @@ import defaultHelpers from './completions/defaultHelpers'
2020
import objectLiteralCompletions from './completions/objectLiteralCompletions'
2121
import filterJsxElements from './completions/filterJsxComponents'
2222
import markOrRemoveGlobalCompletions from './completions/markOrRemoveGlobalLibCompletions'
23-
import { compact, oneOf } from '@zardoy/utils'
23+
import { compact } from '@zardoy/utils'
2424
import adjustAutoImports from './completions/adjustAutoImports'
2525
import escapeStringRegexp from 'escape-string-regexp'
2626
import addSourceDefinition from './completions/addSourceDefinition'
@@ -30,6 +30,7 @@ import changeKindToFunction from './completions/changeKindToFunction'
3030
import functionPropsAndMethods from './completions/functionPropsAndMethods'
3131
import { getTupleSignature } from './tupleSignature'
3232
import stringTemplateTypeCompletions from './completions/stringTemplateType'
33+
import localityBonus from './completions/localityBonus'
3334

3435
export type PrevCompletionMap = Record<
3536
string,
@@ -271,11 +272,13 @@ export const getCompletionsAtPosition = (
271272
}
272273
// #endregion
273274

274-
prior.entries = addSourceDefinition(prior.entries, prevCompletionsMap, c) ?? prior.entries
275+
addSourceDefinition(prior.entries)
275276
displayImportedInfo(prior.entries)
276277

277278
if (c('improveJsxCompletions') && leftNode) prior.entries = improveJsxCompletions(prior.entries, leftNode, position, sourceFile, c('jsxCompletionsMap'))
278279

280+
prior.entries = localityBonus(prior.entries) ?? prior.entries
281+
279282
const processedEntries = new Set<ts.CompletionEntry>()
280283
for (const rule of c('replaceSuggestions')) {
281284
if (rule.filter?.fileNamePattern) {
@@ -376,17 +379,6 @@ export const getCompletionsAtPosition = (
376379
}
377380
}
378381

379-
type ArrayPredicate<T> = (value: T, index: number) => boolean
380-
const arrayMoveItemToFrom = <T>(array: T[], originalItem: ArrayPredicate<T>, itemToMove: ArrayPredicate<T>) => {
381-
const originalItemIndex = array.findIndex(originalItem)
382-
if (originalItemIndex === -1) return undefined
383-
const itemToMoveIndex = array.findIndex(itemToMove)
384-
if (itemToMoveIndex === -1) return undefined
385-
array.splice(originalItemIndex, 0, array[itemToMoveIndex]!)
386-
array.splice(itemToMoveIndex + 1, 1)
387-
return originalItemIndex
388-
}
389-
390382
const patchBuiltinMethods = (c: GetConfig, languageService: ts.LanguageService, isCheckedFile: boolean) => {
391383
if (isTs5() && (isCheckedFile || !c('additionalIncludeExtensions').length)) return
392384

@@ -408,24 +400,21 @@ const patchBuiltinMethods = (c: GetConfig, languageService: ts.LanguageService,
408400
// Its known that fuzzy completion don't work within import completions
409401
// TODO! when file name without with half-ending is typed it doesn't these completions! (seems ts bug, but probably can be fixed here)
410402
// e.g. /styles.css import './styles.c|' - no completions
411-
const oldGetSupportedExtensions = tsFull.getSupportedExtensions
412-
Object.defineProperty(tsFull, 'getSupportedExtensions', {
413-
value: (options, extraFileExtensions) => {
414-
addFileExtensions ??= getAddFileExtensions()
415-
// though I extensions could be just inlined as is
416-
return oldGetSupportedExtensions(
417-
options,
418-
extraFileExtensions?.length
419-
? extraFileExtensions
420-
: addFileExtensions.map(ext => ({
421-
extension: ext,
422-
isMixedContent: true,
423-
scriptKind: ts.ScriptKind.Deferred,
424-
})),
425-
)
426-
},
403+
const unpatch = patchMethod(tsFull, 'getSupportedExtensions', (oldGetSupportedExtensions): any => (options, extraFileExtensions) => {
404+
addFileExtensions ??= getAddFileExtensions()
405+
// though extensions could be just inlined as is
406+
return oldGetSupportedExtensions(
407+
options,
408+
extraFileExtensions?.length
409+
? extraFileExtensions
410+
: addFileExtensions.map(ext => ({
411+
extension: ext,
412+
isMixedContent: true,
413+
scriptKind: ts.ScriptKind.Deferred,
414+
})),
415+
)
427416
})
428417
return () => {
429-
Object.defineProperty(tsFull, 'getSupportedExtensions', { value: oldGetSupportedExtensions })
418+
unpatch()
430419
}
431420
}

0 commit comments

Comments
 (0)