Skip to content

Commit 7fb376d

Browse files
committed
feat: New feature: boost name suggestions! Insanely effective & easy
1 parent b38a3fc commit 7fb376d

File tree

4 files changed

+99
-5
lines changed

4 files changed

+99
-5
lines changed

typescript/src/completions/boostKeywordSuggestions.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { findChildContainingPosition } from '../utils'
1+
import { boostOrAddSuggestions, findChildContainingPosition } from '../utils'
22

33
export default (entries: ts.CompletionEntry[], position: number, node: ts.Node): ts.CompletionEntry[] | undefined => {
44
// todo-not-sure for now, requires explicit completion trigger
@@ -22,8 +22,8 @@ export default (entries: ts.CompletionEntry[], position: number, node: ts.Node):
2222
}
2323
if (extendsKeyword) addOrBoostKeywords.push('extends')
2424
if (addOrBoostKeywords.length === 0) return
25-
return [
26-
...addOrBoostKeywords.map(keyword => ({ name: keyword, kind: ts.ScriptElementKind.keyword, sortText: '07' })),
27-
...entries.filter(({ name }) => !addOrBoostKeywords.includes(name)),
28-
]
25+
return boostOrAddSuggestions(
26+
entries,
27+
addOrBoostKeywords.map(name => ({ name, kind: ts.ScriptElementKind.keyword })),
28+
)
2929
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { boostExistingSuggestions, boostOrAddSuggestions, findChildContainingPosition } from '../utils'
2+
3+
// 1. add suggestions for unresolved indentifiers in code
4+
// 2. boost identifer or type name suggestion
5+
export default (
6+
entries: ts.CompletionEntry[],
7+
position: number,
8+
sourceFile: ts.SourceFile,
9+
node: ts.Node,
10+
languageService: ts.LanguageService,
11+
): ts.CompletionEntry[] | undefined => {
12+
// todo getPreviousPartNode() util
13+
// todo object key
14+
const fileText = sourceFile.getFullText()
15+
const fileTextBeforePos = fileText.slice(0, position)
16+
const preConstNodeOffset = fileTextBeforePos.match(/(?:const|let) ([\w\d]*)$/i)?.[1]
17+
/** false - pick all identifiers after cursor
18+
* node - pick identifiers that within node */
19+
let filterBlock: undefined | false | ts.Node
20+
if (preConstNodeOffset !== undefined) {
21+
const node = findChildContainingPosition(ts, sourceFile, position - preConstNodeOffset.length - 2)
22+
if (!node || !ts.isVariableDeclarationList(node)) return
23+
filterBlock = false
24+
} else if (ts.isIdentifier(node) && node.parent?.parent) {
25+
// node > parent1 > parent2
26+
let parent1 = node.parent
27+
let parent2 = parent1.parent
28+
if (ts.isParameter(parent1) && isFunction(parent2)) {
29+
filterBlock = parent2.body ?? false
30+
}
31+
if (ts.isQualifiedName(parent1)) parent1 = parent1.parent
32+
parent2 = parent1.parent
33+
if (ts.isTypeReferenceNode(parent1) && ts.isParameter(parent2) && isFunction(parent2.parent) && ts.isIdentifier(parent2.name)) {
34+
const name = parent2.name.text.replace(/^_/, '')
35+
// its name convention in TS
36+
const nameUpperFirst = name[0]!.toUpperCase() + name.slice(1)
37+
return boostExistingSuggestions(entries, ({ name }) => {
38+
if (!name.includes(nameUpperFirst)) return false
39+
return true
40+
})
41+
}
42+
}
43+
44+
if (filterBlock === undefined) return
45+
const semanticDiagnostics = languageService.getSemanticDiagnostics(sourceFile.fileName)
46+
47+
const notFoundIdentifiers = semanticDiagnostics
48+
.filter(({ code }) => [2552, 2304].includes(code))
49+
.filter(({ start, length }) => {
50+
if ([start, length].some(x => x === undefined)) return false
51+
if (filterBlock === false) return start! > position
52+
const diagnosticEnd = start! + length!
53+
const { pos, end } = filterBlock!
54+
if (start! < pos) return false
55+
if (diagnosticEnd > end) return false
56+
return true
57+
})
58+
const generalNotFoundNames = [...new Set(notFoundIdentifiers.map(({ start, length }) => fileText.slice(start!, start! + length!)))]
59+
return boostOrAddSuggestions(
60+
entries,
61+
generalNotFoundNames.map(name => ({ name, kind: ts.ScriptElementKind.warning })),
62+
)
63+
64+
function isFunction(node: ts.Node): node is ts.ArrowFunction | ts.FunctionDeclaration {
65+
if (!node) return false
66+
return ts.isArrowFunction(node) || ts.isFunctionDeclaration(node)
67+
}
68+
}

typescript/src/completionsAtPosition.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import objectLiteralHelpers from './completions/objectLiteralHelpers'
1414
import switchCaseExcludeCovered from './completions/switchCaseExcludeCovered'
1515
import additionalTypesSuggestions from './completions/additionalTypesSuggestions'
1616
import boostKeywordSuggestions from './completions/boostKeywordSuggestions'
17+
import boostTextSuggestions from './completions/boostNameSuggestions'
1718

1819
export type PrevCompletionMap = Record<string, { originalName?: string; documentationOverride?: string | ts.SymbolDisplayPart[] }>
1920

@@ -98,6 +99,11 @@ export const getCompletionsAtPosition = (
9899
prior.entries = [...prior.entries, ...addSignatureAccessCompletions]
99100
}
100101

102+
if (leftNode) {
103+
const newEntries = boostTextSuggestions(prior?.entries ?? [], position, sourceFile, leftNode, languageService)
104+
if (newEntries?.length && ensurePrior() && prior) prior.entries = newEntries
105+
}
106+
101107
if (!prior) return
102108

103109
if (c('fixSuggestionsSorting')) prior.entries = fixPropertiesSorting(prior.entries, leftNode, sourceFile, program) ?? prior.entries

typescript/src/utils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type tslib from 'typescript/lib/tsserverlibrary'
2+
import { SetOptional } from 'type-fest'
23

34
export function findChildContainingPosition(
45
typescript: typeof import('typescript/lib/tsserverlibrary'),
@@ -63,6 +64,25 @@ export const cleanupEntryName = ({ name }: Pick<ts.CompletionEntry, 'name'>) =>
6364
return name.replace(/^ /, '')
6465
}
6566

67+
export const boostOrAddSuggestions = (existingEntries: ts.CompletionEntry[], topEntries: SetOptional<ts.CompletionEntry, 'sortText'>[]) => {
68+
const topEntryNames = topEntries.map(({ name }) => name)
69+
return [
70+
...topEntries.map(entry => ({ ...entry, sortText: entry.sortText ?? `07` })),
71+
...existingEntries.filter(({ name }) => !topEntryNames.includes(name)),
72+
]
73+
}
74+
75+
export const boostExistingSuggestions = (entries: ts.CompletionEntry[], predicate: (entry: ts.CompletionEntry) => boolean | number) => {
76+
return [...entries].sort((a, b) => {
77+
return [a, b]
78+
.map(x => {
79+
const res = predicate(x)
80+
return res === true ? 0 : res === false ? 1 : res
81+
})
82+
.reduce((a, b) => a - b)
83+
})
84+
}
85+
6686
// Workaround esbuild bundle modules
6787
export const nodeModules = __WEB__
6888
? null

0 commit comments

Comments
 (0)