Skip to content
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

[typespec-vscode] Add autocomplete of model properties for union type #5483

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
106 changes: 85 additions & 21 deletions packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2549,7 +2549,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
case IdentifierKind.ModelExpressionProperty:
case IdentifierKind.ObjectLiteralProperty:
const model = getReferencedModel(node as ModelPropertyNode | ObjectLiteralPropertyNode);
if (model) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timotheeguerin , @chrisradek , could you help to suggest what we should do here if multiple models are returned here because of union? Shall we change the resolveIdentifier to return an array too?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this method got hijacked a little bit for what it was supposed to do(resolve the identifier this points to) and
is now used to show which identifier it should use for completion.

I think we need to refactor things a little here so that either a new function porvide all the potential linked types or we just expose the completions and the resolution is internal

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't they the same thing? Could you provide more detail about the difference? I mean the resolved model(s) is the same thing for completion or for resolving identifier, isn't it? thanks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well kind of but also not really, it was originally meant as a way to resolve to which declaration an identifier points to so you can do the highlight, complete, etc. but with the addition of the model/value completion it now is looking at the linked type of the value not itself which kinda make this name confusing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, you are right. They are slightly different. But we still have the problem that an identifierNode may be resolved to multiple sym when Union is considered like below, any suggestion on it? shall we update the resolveIdentifier to return sym array? I am a little concern about the impact from that change.
image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timotheeguerin, any suggestion on this? any concern to change resolveIdentifier to return sym[] | undefined? or better suggestion? thanks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah its fine I think, but as its breaking already would also rename it to be a bit more accurate of what its doing

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make sure I understand it correctly. The current logic here is first looking at the linked types for property identifier node, and then after the linked types are found, it will further use getMemberSymbol(...) to get and return the sym of the corresponding property (the identifier node). So do you mean this sym is not a declaration and different from other sym as declaration? Do you have any suggestion for the rename? thanks.

if (model && !Array.isArray(model)) {
sym = getMemberSymbol(model.node!.symbol, id.sv);
} else {
return undefined;
Expand Down Expand Up @@ -2617,7 +2617,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker

function getReferencedModel(
propertyNode: ObjectLiteralPropertyNode | ModelPropertyNode,
): Model | undefined {
): Model | Model[] | undefined {
RodgeFu marked this conversation as resolved.
Show resolved Hide resolved
type ModelOrArrayValueNode = ArrayLiteralNode | ObjectLiteralNode;
type ModelOrArrayTypeNode = ModelExpressionNode | TupleExpressionNode;
type ModelOrArrayNode = ModelOrArrayValueNode | ModelOrArrayTypeNode;
Expand Down Expand Up @@ -2660,7 +2660,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
refType = getReferencedTypeFromConstAssignment(foundNode as ModelOrArrayValueNode);
break;
}
return refType?.kind === "Model" || refType?.kind === "Tuple"

return refType?.kind === "Model" || refType?.kind === "Tuple" || refType?.kind === "Union"
? getNestedModel(refType, path)
: undefined;

Expand All @@ -2679,13 +2680,29 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
) {
path.unshift({ propertyName: node.id.sv });
}
if (node.kind === SyntaxKind.ObjectLiteral) {
RodgeFu marked this conversation as resolved.
Show resolved Hide resolved
// define tupleIndex as -1 to indicate that we are looking for union type
const curResult = path.find((i) => i.tupleIndex === -1);
if (curResult === undefined) {
path.unshift({ tupleIndex: -1 });
} else {
if (path.length > 1) {
// If it is a specific type in the union type, the previous -1 needs to be deleted
const removeIdx = path.findIndex((i) => i.tupleIndex === -1);
if (removeIdx !== -1) {
path.splice(removeIdx, 1);
}
}
}
}
}

function getNestedModel(
modelOrTuple: Model | Tuple | undefined,
modelOrTuple: Model | Tuple | Union | undefined,
path: PathSeg[],
): Model | undefined {
): Model[] | undefined {
let cur: Type | undefined = modelOrTuple;
const models: Model[] = [];
for (const seg of path) {
switch (cur?.kind) {
case "Tuple":
Expand All @@ -2704,15 +2721,50 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
cur = cur.templateMapper?.args[0] as Model;
} else if (cur.name !== "Array" && seg.propertyName) {
cur = cur.properties.get(seg.propertyName)?.type;
if (cur?.kind === "Model") {
const result = getNestedModel(cur, path);
if (result) {
models.push(...result);
}
}
} else if (seg.tupleIndex === -1) {
for (const child of cur.properties.values()) {
if (child.type.kind === "Model") {
const result = getNestedModel(child.type, path);
if (result) {
models.push(...result);
}
}
}
} else {
return undefined;
}

break;
case "Union":
if (seg.tupleIndex === -1 || seg.propertyName) {
// seg.propertyName is empty and contains all,
// otherwise the value contains the model property corresponding to seg.propertyName
for (const variant of cur.variants.values()) {
if (variant.type.kind === "Model") {
const result = getNestedModel(variant.type, path);
if (result) {
models.push(...result);
}
}
}
}
break;
default:
return undefined;
}
}
return cur?.kind === "Model" ? cur : undefined;

if (cur?.kind === "Model") {
models.push(cur);
}

return models.length > 0 ? models : undefined;
}

function getReferencedTypeFromTemplateDeclaration(node: ModelOrArrayNode): Type | undefined {
Expand Down Expand Up @@ -2909,22 +2961,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
return completions;
}
const curModelNode = ancestor.parent as ModelExpressionNode | ObjectLiteralNode;

for (const prop of walkPropertiesInherited(model)) {
if (
identifier.sv === prop.name ||
!curModelNode.properties.find(
(p) =>
(p.kind === SyntaxKind.ModelProperty ||
p.kind === SyntaxKind.ObjectLiteralProperty) &&
p.id.sv === prop.name,
)
) {
const sym = getMemberSymbol(model.node!.symbol, prop.name);
if (sym) {
addCompletion(prop.name, sym);
}
if (Array.isArray(model)) {
for (const curModel of model) {
addInheritedPropertyCompletions(curModel, curModelNode);
}
} else {
addInheritedPropertyCompletions(model, curModelNode);
}
} else if (identifier.parent && identifier.parent.kind === SyntaxKind.MemberExpression) {
let base = resolver.getNodeLinks(identifier.parent.base).resolvedSymbol;
Expand Down Expand Up @@ -2986,6 +3028,28 @@ export function createChecker(program: Program, resolver: NameResolver): Checker

return completions;

function addInheritedPropertyCompletions(
model: Model,
curModelNode: ModelExpressionNode | ObjectLiteralNode,
) {
for (const prop of walkPropertiesInherited(model)) {
if (
identifier.sv === prop.name ||
!curModelNode.properties.find(
(p) =>
(p.kind === SyntaxKind.ModelProperty ||
p.kind === SyntaxKind.ObjectLiteralProperty) &&
p.id.sv === prop.name,
)
) {
const sym = getMemberSymbol(model.node!.symbol, prop.name);
if (sym) {
addCompletion(prop.name, sym);
}
}
}
}

function addCompletions(table: SymbolTable | undefined) {
if (!table) {
return;
Expand Down
130 changes: 130 additions & 0 deletions packages/compiler/test/server/completion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,136 @@ describe("identifiers", () => {
);
});

it("completes union variants(models) of template parameters", async () => {
const completions = await complete(
`
model Options {
a: string;
b: Nested;
}
model Nested {
c:Foo2;
}
model Foo1 {
foo1: string;
}
model Foo2 {
foo2: string;
}

model Test<T extends valueof string | Foo1 | Options> {}

alias A = Test<#{┆}>;
`,
);

check(
completions,
[
{
label: "foo1",
insertText: "foo1",
kind: CompletionItemKind.Field,
documentation: {
kind: MarkupKind.Markdown,
value: "(model property)\n```typespec\nFoo1.foo1: string\n```",
},
},
{
label: "foo2",
insertText: "foo2",
kind: CompletionItemKind.Field,
documentation: {
kind: MarkupKind.Markdown,
value: "(model property)\n```typespec\nFoo2.foo2: string\n```",
},
},
{
label: "c",
insertText: "c",
kind: CompletionItemKind.Field,
documentation: {
kind: MarkupKind.Markdown,
value: "(model property)\n```typespec\nNested.c: Foo2\n```",
},
},
{
label: "a",
insertText: "a",
kind: CompletionItemKind.Field,
documentation: {
kind: MarkupKind.Markdown,
value: "(model property)\n```typespec\nOptions.a: string\n```",
},
},
{
label: "b",
insertText: "b",
kind: CompletionItemKind.Field,
documentation: {
kind: MarkupKind.Markdown,
value: "(model property)\n```typespec\nOptions.b: Nested\n```",
},
},
],
{
allowAdditionalCompletions: false,
},
);
});

it("completes specific type in union variants(models) of template parameters", async () => {
const completions = await complete(
`
model Options {
a: string;
b: Nested;
}
model Nested {
c:Foo2;
d:string;
}
model Foo1 {
foo1: string;
}
model Foo2 {
foo2: string;
}

model Test<T extends valueof string | Foo1 | Options> {}

alias A = Test<#{a:"",b:#{┆}}>;
`,
);

check(
completions,
[
{
label: "c",
insertText: "c",
kind: CompletionItemKind.Field,
documentation: {
kind: MarkupKind.Markdown,
value: "(model property)\n```typespec\nNested.c: Foo2\n```",
},
},
{
label: "d",
insertText: "d",
kind: CompletionItemKind.Field,
documentation: {
kind: MarkupKind.Markdown,
value: "(model property)\n```typespec\nNested.d: string\n```",
},
},
],
{
allowAdditionalCompletions: false,
},
);
});

it("completes namespace operations", async () => {
const completions = await complete(
`
Expand Down
Loading