|
| 1 | +export default (position: number, node: ts.Node | undefined, sourceFile: ts.SourceFile, program: ts.Program, languageService: ts.LanguageService) => { |
| 2 | + if (!node) return |
| 3 | + function isBinaryExpression(node: ts.Node): node is ts.BinaryExpression { |
| 4 | + return node.kind === ts.SyntaxKind.BinaryExpression |
| 5 | + } |
| 6 | + const typeChecker = program.getTypeChecker() |
| 7 | + // TODO info diagnostic if used that doesn't exist |
| 8 | + if ( |
| 9 | + ts.isStringLiteralLike(node) && |
| 10 | + isBinaryExpression(node.parent) && |
| 11 | + node.parent.left === node && |
| 12 | + node.parent.operatorToken.kind === ts.SyntaxKind.InKeyword |
| 13 | + ) { |
| 14 | + const quote = node.getText()[0]! |
| 15 | + const type = typeChecker.getTypeAtLocation(node.parent.right) |
| 16 | + const suggestionsData = new Map< |
| 17 | + string, |
| 18 | + { |
| 19 | + insertText: string |
| 20 | + usingDisplayIndexes: number[] |
| 21 | + documentations: string[] |
| 22 | + } |
| 23 | + >() |
| 24 | + const types = type.isUnion() ? type.types : [type] |
| 25 | + for (let [typeIndex, typeEntry] of types.entries()) { |
| 26 | + // improved DX: not breaking other completions as TS would display error anyway |
| 27 | + if (!(typeEntry.flags & ts.TypeFlags.Object)) continue |
| 28 | + for (const prop of typeEntry.getProperties()) { |
| 29 | + const { name } = prop |
| 30 | + if (!suggestionsData.has(name)) { |
| 31 | + suggestionsData.set(name, { |
| 32 | + insertText: prop.name.replace(quote, `\\${quote}`), |
| 33 | + usingDisplayIndexes: [], |
| 34 | + documentations: [], |
| 35 | + }) |
| 36 | + } |
| 37 | + suggestionsData.get(name)!.usingDisplayIndexes.push(typeIndex + 1) |
| 38 | + // const doc = prop.getDocumentationComment(typeChecker) |
| 39 | + const declaration = prop.getDeclarations()?.[0] |
| 40 | + suggestionsData |
| 41 | + .get(name)! |
| 42 | + .documentations.push(`${typeIndex + 1}: ${declaration && typeChecker.typeToString(typeChecker.getTypeAtLocation(declaration))}`) |
| 43 | + } |
| 44 | + } |
| 45 | + const docPerCompletion: Record<string, string> = {} |
| 46 | + const maxUsingDisplayIndex = Math.max(...[...suggestionsData.entries()].map(([, { usingDisplayIndexes }]) => usingDisplayIndexes.length)) |
| 47 | + const completions: ts.CompletionEntry[] = [...suggestionsData.entries()] |
| 48 | + .map(([originaName, { insertText, usingDisplayIndexes, documentations }], i) => { |
| 49 | + const name = types.length > 1 && usingDisplayIndexes.length === 1 ? `☆${originaName}` : originaName |
| 50 | + docPerCompletion[name] = documentations.join('\n\n') |
| 51 | + return { |
| 52 | + // ⚀ ⚁ ⚂ ⚃ ⚄ ⚅ |
| 53 | + name, |
| 54 | + kind: ts.ScriptElementKind.string, |
| 55 | + insertText, |
| 56 | + sourceDisplay: [{ kind: 'text', text: usingDisplayIndexes.join(', ') }], |
| 57 | + sortText: `${maxUsingDisplayIndex - usingDisplayIndexes.length}_${i}`, |
| 58 | + } |
| 59 | + }) |
| 60 | + .sort((a, b) => a.sortText.localeCompare(b.sortText)) |
| 61 | + return { |
| 62 | + completions, |
| 63 | + docPerCompletion, |
| 64 | + } |
| 65 | + } |
| 66 | + return |
| 67 | +} |
0 commit comments