-
Notifications
You must be signed in to change notification settings - Fork 12.8k
No completions from tsserver
for property values in generic type arguments
#56299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
It looks like a strong candidate for being a duplicate of #52898 |
@Andarist The issue described there seems to be a much more severe lack of completions for type arguments to functions. However, it is specific to functions. The issue described there does not apply to type references, which my issue relates to. Here is a simplified example that demonstrates that functions get no completions at all for type arguments while type references do: type OneOrTwo = "one" | "two"
// Completions do not work for functions
declare function MyFunction<T extends OneOrTwo>(): T
MyFunction<'no completions here'>()
// Completions DO work for type references
type MyType<T extends OneOrTwo> = T
MyType<'completions here'> That issue actually seems a lot more straightforward to fix because I can't think of a reason why you couldn't route completion requests for type arguments to functions down the same code path as type arguments to type references. But maybe that's overly optimistic. |
The core problem starts here in Intuitively, we'd want to handle this the same way we handle the case for switch (grandParent.kind) {
// ...
case SyntaxKind.TypeReference: {
const typeArgument = findAncestor(parent, n => n.parent === grandParent) as LiteralTypeNode;
if (typeArgument) {
return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(typeArgument)), isNewIdentifier: false };
}
return undefined;
}
// ...
} However, it doesn't seem trivial because the type checker is also not expecting this node hierarchy. Here in The type checker is clearly already aware of the constraints of the string property because it is correctly showing |
I'd like to attempt a fix but could use some guidance. In the meantime, I hacked together a import type tss from 'typescript/lib/tsserverlibrary'
// TODO: Shouldn't be hacking internal APIs
declare module 'typescript/lib/tsserverlibrary' {
function findTokenOnLeftOfPosition(sourceFile: tss.SourceFile, position: number): tss.Node
function createTextSpanFromStringLiteralLikeContent(node: tss.StringLiteralLike): tss.TextSpan
function createTextSpanFromNode(node: tss.Node): tss.TextSpan
function quote(sourceFile: tss.SourceFile, preferences: tss.UserPreferences, text: string): string
interface TypeChecker {
getTypeArgumentConstraint(node: tss.Node): tss.Type
}
}
class Info {
constructor(
readonly pos: number,
readonly ts: typeof tss,
readonly tc: tss.TypeChecker,
readonly ls: tss.LanguageService,
readonly program: tss.Program,
readonly sourceFile: tss.SourceFile,
readonly options: tss.GetCompletionsAtPositionOptions = { quotePreference: 'auto' },
readonly formatting: tss.FormatCodeSettings = {}
) {}
}
export = ({ typescript }: { typescript: typeof tss }) => {
return {
create({ project, languageService }: tss.server.PluginCreateInfo) {
project.projectService.logger.info('Fluent TS Server Plugin initialized')
return createLanguageServiceProxy(typescript, languageService)
},
}
}
function createLanguageServiceProxy(ts: typeof tss, ls: tss.LanguageService) {
const proxy: tss.LanguageService = Object.create(ls)
proxy.getCompletionsAtPosition = (fileName, pos, options, formatting) => {
const program = ls.getProgram()
const file = program?.getSourceFile(fileName)
const tc = program?.getTypeChecker()
const completions = ls.getCompletionsAtPosition(fileName, pos, options, formatting)
return program && file && tc
? getCompletions(new Info(pos, ts, tc, ls, program, file, options, formatting), completions)
: completions
}
return proxy
}
function getCompletions(info: Info, completions = createEmptyCompletions()) {
const contextToken = info.ts.findTokenOnLeftOfPosition(info.sourceFile, info.pos)
const property = info.ts.findAncestor(contextToken, info.ts.isPropertySignature)
if (property && info.ts.findAncestor(contextToken, info.ts.isTypeReferenceNode)) {
const constraint = getTypeArgumentConstraint(contextToken, info)
if (constraint) {
const types = constraint.isUnionOrIntersection() ? constraint.types : [constraint]
completions.entries.push(...getCompletionEntries(contextToken, types, info))
}
}
return completions
}
function createEmptyCompletions(): tss.CompletionInfo {
return {
entries: [],
isGlobalCompletion: false,
isMemberCompletion: false,
isNewIdentifierLocation: false,
}
}
function getTypeArgumentConstraint(node: tss.Node, info: Info): tss.Type | undefined {
const { ts, tc } = info
if (ts.isTypeReferenceNode(node.parent)) {
return tc.getTypeArgumentConstraint(node)
} else if (ts.isPropertySignature(node.parent)) {
const typeLiteral = node.parent.parent
if (ts.isTypeLiteralNode(typeLiteral)) {
const typeLiteralConstraint = getTypeArgumentConstraint(typeLiteral, info)
const propertyConstraint = typeLiteralConstraint?.getProperty(node.parent.name.getText())
if (propertyConstraint) {
return tc.getTypeOfSymbolAtLocation(propertyConstraint, node.parent)
}
}
}
return node.parent ? getTypeArgumentConstraint(node.parent, info) : undefined
}
function getCompletionEntries(contextToken: tss.Node, types: tss.Type[], info: Info): tss.CompletionEntry[] {
const entries: tss.CompletionEntry[] = []
for (const type of types) {
if (type.isLiteral()) {
entries.push({
kind: info.ts.ScriptElementKind.string,
kindModifiers: info.ts.ScriptElementKindModifier.none,
name: getEntryText(contextToken, type.value, info),
sortText: '0', // Display at top
replacementSpan: getReplacementSpan(contextToken, info),
})
}
}
return entries
}
function getEntryText(contextToken: tss.Node, value: tss.LiteralType['value'], info: Info) {
return !info.ts.isStringLiteralLike(contextToken) && typeof value === 'string'
? info.ts.quote(info.sourceFile, info.options, value)
: value.toString()
}
function getReplacementSpan(contextToken: tss.Node, { ts, pos }: Info) {
if (ts.isStringLiteralLike(contextToken)) {
return ts.createTextSpanFromStringLiteralLikeContent(contextToken) // Replace content of string
} else if (ts.isLiteralTypeLiteral(contextToken)) {
return ts.createTextSpanFromNode(contextToken) // Replace entire literal value
} else {
return ts.createTextSpan(pos, 0) // Insert at position without replacing
}
} For now it'll do as a workaround but I don't feel great about this as long-term solution. Some of the internal APIs in the Edit: I updated the plugin code above. It now supports a lot more usage scenarios and works for everything I tested. |
If u are interested in contributing, i can offer some guidance around the codebase etc. Feel free to DM me |
@Andarist Yes I would greatly appreciate that! I will reach out. I updated the plugin code above in an attempt to achieve the idea I had about deriving sub-constraints for nested type literals. It works great, but I had to utilize some internal APIs to achieve it. Here's the critical code section: function getTypeArgumentConstraint(node: tss.Node, info: Info): tss.Type | undefined {
const { ts, tc } = info
if (ts.isTypeReferenceNode(node.parent)) {
return tc.getTypeArgumentConstraint(node)
} else if (ts.isPropertySignature(node.parent)) {
const typeLiteral = node.parent.parent
if (ts.isTypeLiteralNode(typeLiteral)) {
const typeLiteralConstraint = getTypeArgumentConstraint(typeLiteral, info)
const propertyConstraint = typeLiteralConstraint?.getProperty(node.parent.name.getText())
if (propertyConstraint) {
return tc.getTypeOfSymbolAtLocation(propertyConstraint, node.parent)
}
}
}
return node.parent ? getTypeArgumentConstraint(node.parent, info) : undefined
} It recursively walks up to the nearest When I find time I'd love to take a stab at a permanent fix, but does this seem like generally the right direction or am I way off? |
🔎 Search Terms
"tsserver", "generic object completion", "generic intellisense", "generic typeahead", "generic completion", "generic object properties", "object property completion", "typescript server", "typescript lsp"
🕗 Version & Regression Information
This is the behavior in every version I tried, and I reviewed the FAQ for entries about typeahead support for generic type arguments.
⏯ Playground Link
https://www.typescriptlang.org/play?#code/C4TwDgpgBAKuEEEA8MoQB7AgOwCYGcoByAe2wiKgB9jgB3EogPigF5YAoUSWeAIRRpMOAlADeUMACcSYAFzEyFarQaUAvi3YwOHAPR6ogUHIoAcRIlcCgMIkAtmAA2EYAEsyhaSQBur3BFwueCgAUXQAQwdnBDZeSGQiZl0DKEAZcig+cKsoWyiXd2xCADkAeVQvX39A7mgwyKcIPli4SAEJL3liDSYgA
💻 Code
🙁 Actual behavior
Getting completions between the quotes on line 5 (using
Control + Space
on Mac OS) correctly provides'one'
and'two'
as options.However, doing the same between the quotes on line 8 provides no completions.
🙂 Expected behavior
Getting completions between the quotes on both lines 5 and 8 (using
Control + Space
on Mac OS) correctly provides'one'
and'two'
as options.Additional information about the issue
The type of
prop
is correct ('one' | 'two'
) and if you enter an invalid value you get the correct type error. Therefore, TypeScript should have sufficient information to provide the exact same completions in both examples.I enabled verbose
tsserver
logging in Visual Studio Code and captured the requests and responses for these completion requests.Line 5 Request:
Line 5 Response:
Line 8 Request:
Line 8 Response:
Line 8 Logs:
As you can see, the requests are both identical except for position. But the response for the request on line 8 is empty, and there is a message indicating the position is invalid.
The text was updated successfully, but these errors were encountered: