diff --git a/.chronus/changes/add-completion-for-decorator-model-arg-2024-4-17-11-50-34.md b/.chronus/changes/add-completion-for-decorator-model-arg-2024-4-17-11-50-34.md new file mode 100644 index 0000000000..993f754828 --- /dev/null +++ b/.chronus/changes/add-completion-for-decorator-model-arg-2024-4-17-11-50-34.md @@ -0,0 +1,21 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Support completion for Model with extended properties + + Example + ```tsp + model Device { + name: string; + description: string; + } + + model Phone extends Device { + ┆ + } | [name] + | [description] + ``` + diff --git a/.chronus/changes/add-completion-for-decorator-model-arg-2024-4-6-16-27-17.md b/.chronus/changes/add-completion-for-decorator-model-arg-2024-4-6-16-27-17.md new file mode 100644 index 0000000000..6318a4f984 --- /dev/null +++ b/.chronus/changes/add-completion-for-decorator-model-arg-2024-4-6-16-27-17.md @@ -0,0 +1,21 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Support completion for object values and model expression properties. + + Example + ```tsp + model User { + name: string; + age: int32; + address: string; + } + + const user: User = #{name: "Bob", ┆} + | [age] + | [address] + ``` + diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 01d5470186..246d4e3e84 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -40,6 +40,7 @@ import { numericRanges } from "./numeric-ranges.js"; import { Numeric } from "./numeric.js"; import { exprIsBareIdentifier, + getFirstAncestor, getIdentifierContext, hasParseError, visitChildren, @@ -130,6 +131,7 @@ import { NumericLiteralNode, NumericValue, ObjectLiteralNode, + ObjectLiteralPropertyNode, ObjectValue, ObjectValuePropertyDescriptor, Operation, @@ -2711,6 +2713,16 @@ export function createChecker(program: Program): Checker { const { node, kind } = getIdentifierContext(id); switch (kind) { + case IdentifierKind.ModelExpressionProperty: + case IdentifierKind.ObjectLiteralProperty: + const model = getReferencedModel(node as ModelPropertyNode | ObjectLiteralPropertyNode); + if (model) { + sym = getMemberSymbol(model.node!.symbol, id.sv); + } else { + return undefined; + } + break; + case IdentifierKind.ModelStatementProperty: case IdentifierKind.Declaration: if (node.symbol && (!isTemplatedNode(node) || mapper === undefined)) { sym = getMergedSymbol(node.symbol); @@ -2792,6 +2804,219 @@ export function createChecker(program: Program): Checker { return (resolved?.declarations.filter((n) => isTemplatedNode(n)) ?? []) as TemplateableNode[]; } + function getReferencedModel( + propertyNode: ObjectLiteralPropertyNode | ModelPropertyNode + ): Model | undefined { + type ModelOrArrayValueNode = ArrayLiteralNode | ObjectLiteralNode; + type ModelOrArrayTypeNode = ModelExpressionNode | TupleExpressionNode; + type ModelOrArrayNode = ModelOrArrayValueNode | ModelOrArrayTypeNode; + type PathSeg = { propertyName?: string; tupleIndex?: number }; + const isModelOrArrayValue = (n: Node | undefined) => + n?.kind === SyntaxKind.ArrayLiteral || n?.kind === SyntaxKind.ObjectLiteral; + const isModelOrArrayType = (n: Node | undefined) => + n?.kind === SyntaxKind.ModelExpression || n?.kind === SyntaxKind.TupleExpression; + const isModelOrArray = (n: Node | undefined) => isModelOrArrayValue(n) || isModelOrArrayType(n); + + const path: PathSeg[] = []; + let preNode: Node | undefined; + const foundNode = getFirstAncestor(propertyNode, (n) => { + pushToModelPath(n, preNode, path); + preNode = n; + return ( + (isModelOrArray(n) && + (n.parent?.kind === SyntaxKind.TemplateParameterDeclaration || + n.parent?.kind === SyntaxKind.DecoratorExpression)) || + (isModelOrArrayValue(n) && + (n.parent?.kind === SyntaxKind.CallExpression || + n.parent?.kind === SyntaxKind.ConstStatement)) + ); + }); + + let refType: Type | undefined; + switch (foundNode?.parent?.kind) { + case SyntaxKind.TemplateParameterDeclaration: + refType = getReferencedTypeFromTemplateDeclaration(foundNode as ModelOrArrayNode); + break; + case SyntaxKind.DecoratorExpression: + refType = getReferencedTypeFromDecoratorArgument(foundNode as ModelOrArrayNode); + break; + case SyntaxKind.CallExpression: + refType = getReferencedTypeFromScalarConstructor(foundNode as ModelOrArrayValueNode); + break; + case SyntaxKind.ConstStatement: + refType = getReferencedTypeFromConstAssignment(foundNode as ModelOrArrayValueNode); + break; + } + return refType?.kind === "Model" || refType?.kind === "Tuple" + ? getNestedModel(refType, path) + : undefined; + + function pushToModelPath(node: Node, preNode: Node | undefined, path: PathSeg[]) { + if (node.kind === SyntaxKind.ArrayLiteral || node.kind === SyntaxKind.TupleExpression) { + const index = node.values.findIndex((n) => n === preNode); + if (index >= 0) { + path.unshift({ tupleIndex: index }); + } else { + compilerAssert(false, "not expected, can't find child from the parent?"); + } + } + if ( + node.kind === SyntaxKind.ModelProperty || + node.kind === SyntaxKind.ObjectLiteralProperty + ) { + path.unshift({ propertyName: node.id.sv }); + } + } + + function getNestedModel( + modelOrTuple: Model | Tuple | undefined, + path: PathSeg[] + ): Model | undefined { + let cur: Type | undefined = modelOrTuple; + for (const seg of path) { + switch (cur?.kind) { + case "Tuple": + if ( + seg.tupleIndex !== undefined && + seg.tupleIndex >= 0 && + seg.tupleIndex < cur.values.length + ) { + cur = cur.values[seg.tupleIndex]; + } else { + return undefined; + } + break; + case "Model": + if (cur.name === "Array" && seg.tupleIndex !== undefined) { + cur = cur.templateMapper?.args[0] as Model; + } else if (cur.name !== "Array" && seg.propertyName) { + cur = cur.properties.get(seg.propertyName)?.type; + } else { + return undefined; + } + break; + default: + return undefined; + } + } + return cur?.kind === "Model" ? cur : undefined; + } + + function getReferencedTypeFromTemplateDeclaration(dftNode: ModelOrArrayNode): Type | undefined { + const templateParmaeterDeclNode = dftNode?.parent; + if ( + templateParmaeterDeclNode?.kind !== SyntaxKind.TemplateParameterDeclaration || + !templateParmaeterDeclNode.constraint || + !templateParmaeterDeclNode.default || + templateParmaeterDeclNode.default !== dftNode + ) { + return undefined; + } + + let constraintType: Type | undefined; + if ( + isModelOrArrayValue(dftNode) && + templateParmaeterDeclNode.constraint.kind === SyntaxKind.ValueOfExpression + ) { + constraintType = program.checker.getTypeForNode( + templateParmaeterDeclNode.constraint.target + ); + } else if ( + isModelOrArrayType(dftNode) && + templateParmaeterDeclNode.constraint.kind !== SyntaxKind.ValueOfExpression + ) { + constraintType = program.checker.getTypeForNode(templateParmaeterDeclNode.constraint); + } + + return constraintType; + } + + function getReferencedTypeFromScalarConstructor( + argNode: ModelOrArrayValueNode + ): Type | undefined { + const callExpNode = argNode?.parent; + if (callExpNode?.kind !== SyntaxKind.CallExpression) { + return undefined; + } + + const ctorType = checkCallExpressionTarget(callExpNode, undefined); + + if (ctorType?.kind !== "ScalarConstructor") { + return undefined; + } + + const argIndex = callExpNode.arguments.findIndex((n) => n === argNode); + if (argIndex < 0 || argIndex >= ctorType.parameters.length) { + return undefined; + } + const arg = ctorType.parameters[argIndex]; + + return arg.type; + } + + function getReferencedTypeFromConstAssignment( + valueNode: ModelOrArrayValueNode + ): Type | undefined { + const constNode = valueNode?.parent; + if ( + !constNode || + constNode.kind !== SyntaxKind.ConstStatement || + !constNode.type || + constNode.value !== valueNode + ) { + return undefined; + } + + return program.checker.getTypeForNode(constNode.type); + } + + function getReferencedTypeFromDecoratorArgument( + decArgNode: ModelOrArrayNode + ): Type | undefined { + const decNode = decArgNode?.parent; + if (decNode?.kind !== SyntaxKind.DecoratorExpression) { + return undefined; + } + + const decSym = program.checker.resolveIdentifier( + decNode.target.kind === SyntaxKind.MemberExpression ? decNode.target.id : decNode.target + ); + if (!decSym) { + return undefined; + } + + const decDecl: DecoratorDeclarationStatementNode | undefined = decSym.declarations.find( + (x): x is DecoratorDeclarationStatementNode => + x.kind === SyntaxKind.DecoratorDeclarationStatement + ); + if (!decDecl) { + return undefined; + } + + const decType = program.checker.getTypeForNode(decDecl); + compilerAssert(decType.kind === "Decorator", "Expected type to be a Decorator."); + + const argIndex = decNode.arguments.findIndex((n) => n === decArgNode); + if (argIndex < 0 || argIndex >= decType.parameters.length) { + return undefined; + } + const decArg = decType.parameters[argIndex]; + + let type: Type | undefined; + if (isModelOrArrayValue(decArgNode)) { + type = decArg.type.valueType; + } else if (isModelOrArrayType(decArgNode)) { + type = decArg.type.type ?? decArg.type.valueType; + } else { + compilerAssert( + false, + "not expected node type to get reference model from decorator argument" + ); + } + return type; + } + } + function resolveCompletions(identifier: IdentifierNode): Map { const completions = new Map(); const { kind, node: ancestor } = getIdentifierContext(identifier); @@ -2801,6 +3026,9 @@ export function createChecker(program: Program): Checker { case IdentifierKind.Decorator: case IdentifierKind.Function: case IdentifierKind.TypeReference: + case IdentifierKind.ModelExpressionProperty: + case IdentifierKind.ModelStatementProperty: + case IdentifierKind.ObjectLiteralProperty: break; // supported case IdentifierKind.Other: return completions; // not implemented @@ -2825,7 +3053,49 @@ export function createChecker(program: Program): Checker { compilerAssert(false, "Unreachable"); } - if (identifier.parent && identifier.parent.kind === SyntaxKind.MemberExpression) { + if (kind === IdentifierKind.ModelStatementProperty) { + const model = ancestor.parent as ModelStatementNode; + const modelType = program.checker.getTypeForNode(model) as Model; + const baseType = modelType.baseModel; + const baseNode = baseType?.node; + if (!baseNode) { + return completions; + } + for (const prop of baseType.properties.values()) { + if (identifier.sv === prop.name || !modelType.properties.has(prop.name)) { + const sym = getMemberSymbol(baseNode.symbol, prop.name); + if (sym) { + addCompletion(prop.name, sym); + } + } + } + } else if ( + kind === IdentifierKind.ModelExpressionProperty || + kind === IdentifierKind.ObjectLiteralProperty + ) { + const model = getReferencedModel(ancestor as ModelPropertyNode | ObjectLiteralPropertyNode); + if (!model) { + 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); + } + } + } + } else if (identifier.parent && identifier.parent.kind === SyntaxKind.MemberExpression) { let base = resolveTypeReferenceSym(identifier.parent.base, undefined, false); if (base) { if (base.flags & SymbolFlags.Alias) { @@ -2930,6 +3200,10 @@ export function createChecker(program: Program): Checker { function shouldAddCompletion(sym: Sym): boolean { switch (kind) { + case IdentifierKind.ModelExpressionProperty: + case IdentifierKind.ModelStatementProperty: + case IdentifierKind.ObjectLiteralProperty: + return !!(sym.flags & SymbolFlags.ModelProperty); case IdentifierKind.Decorator: // Only return decorators and namespaces when completing decorator return !!(sym.flags & (SymbolFlags.Decorator | SymbolFlags.Namespace)); diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 0f18aa39be..12dcc2cf19 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -71,6 +71,7 @@ import { OperationSignature, OperationStatementNode, ParseOptions, + PositionDetail, ProjectionBlockExpressionNode, ProjectionEnumMemberSelectorNode, ProjectionEnumSelectorNode, @@ -3712,6 +3713,28 @@ function visitEach(cb: NodeCallback, nodes: readonly Node[] | undefined): return; } +export function getNodeAtPositionDetail( + script: TypeSpecScriptNode, + position: number, + filter?: (node: Node) => boolean +): PositionDetail | undefined { + const node = getNodeAtPosition(script, position, filter); + if (!node) return undefined; + + const char = script.file.text.charCodeAt(position); + const preChar = position >= 0 ? script.file.text.charCodeAt(position - 1) : NaN; + const nextChar = + position < script.file.text.length ? script.file.text.charCodeAt(position + 1) : NaN; + + return { + node, + position, + preChar, + nextChar, + char, + }; +} + /** * Resolve the node in the syntax tree that that is at the given position. * @param script TypeSpec Script node @@ -3844,6 +3867,26 @@ export function getIdentifierContext(id: IdentifierNode): IdentifierContext { case SyntaxKind.TemplateArgument: kind = IdentifierKind.TemplateArgument; break; + case SyntaxKind.ObjectLiteralProperty: + kind = IdentifierKind.ObjectLiteralProperty; + break; + case SyntaxKind.ModelProperty: + switch (node.parent?.kind) { + case SyntaxKind.ModelExpression: + kind = IdentifierKind.ModelExpressionProperty; + break; + case SyntaxKind.ModelStatement: + kind = IdentifierKind.ModelStatementProperty; + break; + default: + compilerAssert("false", "ModelProperty with unexpected parent kind."); + kind = + (id.parent as DeclarationNode).id === id + ? IdentifierKind.Declaration + : IdentifierKind.Other; + break; + } + break; default: kind = (id.parent as DeclarationNode).id === id diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 133681deed..acce9553f9 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -1046,6 +1046,17 @@ export interface TemplateDeclarationNode { readonly locals?: SymbolTable; } +/** + * owner node and other related information according to the position + */ +export interface PositionDetail { + readonly node: Node; + readonly position: number; + readonly char: number; + readonly preChar: number; + readonly nextChar: number; +} + export type Node = | TypeSpecScriptNode | JsSourceFileNode @@ -1869,6 +1880,9 @@ export enum IdentifierKind { Function, Using, Declaration, + ModelExpressionProperty, + ModelStatementProperty, + ObjectLiteralProperty, Other, } diff --git a/packages/compiler/src/server/completion.ts b/packages/compiler/src/server/completion.ts index 92d9cbd856..44a1284f70 100644 --- a/packages/compiler/src/server/completion.ts +++ b/packages/compiler/src/server/completion.ts @@ -11,8 +11,9 @@ import { getDeprecationDetails } from "../core/deprecation.js"; import { CompilerHost, IdentifierNode, - Node, + NodeFlags, NodePackage, + PositionDetail, Program, StringLiteralNode, SymbolFlags, @@ -40,9 +41,11 @@ export type CompletionContext = { export async function resolveCompletion( context: CompletionContext, - node: Node | undefined + posDetail: PositionDetail | undefined ): Promise { + const node = posDetail?.node; if ( + posDetail === undefined || node === undefined || node.kind === SyntaxKind.InvalidStatement || (node.kind === SyntaxKind.Identifier && @@ -67,6 +70,11 @@ export async function resolveCompletion( await addImportCompletion(context, node); } break; + case SyntaxKind.ModelStatement: + case SyntaxKind.ObjectLiteral: + case SyntaxKind.ModelExpression: + addModelCompletion(context, posDetail); + break; } } @@ -237,6 +245,39 @@ async function addRelativePathCompletion( } } +function addModelCompletion(context: CompletionContext, posDetail: PositionDetail) { + const node = posDetail.node; + if ( + node.kind !== SyntaxKind.ModelStatement && + node.kind !== SyntaxKind.ModelExpression && + node.kind !== SyntaxKind.ObjectLiteral + ) { + return; + } + // skip the scenario like `{ ... }|` + if (node.end === posDetail.position) { + return; + } + // create a fake identifier node to further resolve the completions for the model/object + // it's a little tricky but can help to keep things clean and simple while the cons. is limited + // TODO: consider adding support in resolveCompletions for non-identifier-node directly when we find more scenario and worth the cost + const fakeProp = { + kind: + node.kind === SyntaxKind.ObjectLiteral + ? SyntaxKind.ObjectLiteralProperty + : SyntaxKind.ModelProperty, + flags: NodeFlags.None, + parent: node, + }; + const fakeId = { + kind: SyntaxKind.Identifier, + sv: "", + flags: NodeFlags.None, + parent: fakeProp, + }; + addIdentifierCompletion(context, fakeId as IdentifierNode); +} + /** * Add completion options for an identifier. */ diff --git a/packages/compiler/src/server/serverlib.ts b/packages/compiler/src/server/serverlib.ts index 18f92791b7..f6a340b5bf 100644 --- a/packages/compiler/src/server/serverlib.ts +++ b/packages/compiler/src/server/serverlib.ts @@ -52,7 +52,7 @@ import { formatTypeSpec } from "../core/formatter.js"; import { getEntityName, getTypeName } from "../core/helpers/type-name-utils.js"; import { ResolveModuleHost, resolveModule } from "../core/index.js"; import { getPositionBeforeTrivia } from "../core/parser-utils.js"; -import { getNodeAtPosition, visitChildren } from "../core/parser.js"; +import { getNodeAtPosition, getNodeAtPositionDetail, visitChildren } from "../core/parser.js"; import { ensureTrailingDirectorySeparator, getDirectoryPath } from "../core/path-utils.js"; import type { Program } from "../core/program.js"; import { skipTrivia, skipWhiteSpace } from "../core/scanner.js"; @@ -67,6 +67,7 @@ import { DiagnosticTarget, IdentifierNode, Node, + PositionDetail, SourceFile, SyntaxKind, TextRange, @@ -670,7 +671,7 @@ export function createServer(host: ServerHost): Server { const result = await compileService.compile(params.textDocument); if (result) { const { script, document, program } = result; - const node = getCompletionNodeAtPosition(script, document.offsetAt(params.position)); + const posDetail = getCompletionNodeAtPosition(script, document.offsetAt(params.position)); return await resolveCompletion( { @@ -679,7 +680,7 @@ export function createServer(host: ServerHost): Server { completions, params, }, - node + posDetail ); } @@ -1076,10 +1077,10 @@ export function getCompletionNodeAtPosition( script: TypeSpecScriptNode, position: number, filter: (node: Node) => boolean = (node: Node) => true -): Node | undefined { - const realNode = getNodeAtPosition(script, position, filter); - if (realNode?.kind === SyntaxKind.StringLiteral) { - return realNode; +): PositionDetail | undefined { + const detail = getNodeAtPositionDetail(script, position, filter); + if (detail?.node.kind === SyntaxKind.StringLiteral) { + return detail; } // If we're not immediately after an identifier character, then advance // the position past any trivia. This is done because a zero-width @@ -1089,8 +1090,8 @@ export function getCompletionNodeAtPosition( if (!cp || !isIdentifierContinue(cp)) { const newPosition = skipTrivia(script.file.text, position); if (newPosition !== position) { - return getNodeAtPosition(script, newPosition, filter); + return getNodeAtPositionDetail(script, newPosition, filter); } } - return realNode; + return detail; } diff --git a/packages/compiler/src/server/type-details.ts b/packages/compiler/src/server/type-details.ts index 2763bd741d..44ed2790c0 100644 --- a/packages/compiler/src/server/type-details.ts +++ b/packages/compiler/src/server/type-details.ts @@ -2,6 +2,7 @@ import { compilerAssert, DocContent, getDocData, + isType, Node, Program, Sym, @@ -62,11 +63,19 @@ function getSymbolDocumentation(program: Program, symbol: Sym) { } // Add @doc(...) API docs - const type = symbol.type ?? program.checker.getTypeForNode(symbol.declarations[0]); - const apiDocs = getDocData(program, type); - // The doc comment is already included above we don't want to duplicate. Only include if it was specificed via `@doc` - if (apiDocs && apiDocs.source === "decorator") { - docs.push(apiDocs.value); + let type = symbol.type; + if (!type) { + const entity = program.checker.getTypeOrValueForNode(symbol.declarations[0]); + if (entity && isType(entity)) { + type = entity; + } + } + if (type) { + const apiDocs = getDocData(program, type); + // The doc comment is already included above we don't want to duplicate. Only include if it was specificed via `@doc` + if (apiDocs && apiDocs.source === "decorator") { + docs.push(apiDocs.value); + } } return docs.join("\n\n"); diff --git a/packages/compiler/test/server/completion.test.ts b/packages/compiler/test/server/completion.test.ts index 0c21962680..90b78e8f71 100644 --- a/packages/compiler/test/server/completion.test.ts +++ b/packages/compiler/test/server/completion.test.ts @@ -640,6 +640,91 @@ describe("identifiers", () => { ); }); + it("completes extended model properties", async () => { + const completions = await complete( + ` + model N { + name: string; + value: int16 + } + model M extends N { + test: string; + ┆ + } + ` + ); + + check( + completions, + [ + { + label: "name", + insertText: "name", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nN.name: string\n```", + }, + }, + { + label: "value", + insertText: "value", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nN.value: int16\n```", + }, + }, + ], + { + allowAdditionalCompletions: false, + } + ); + }); + + it("completes extended model typing and remaining properties", async () => { + const completions = await complete( + ` + model N { + name: string; + value: int16; + extra: boolean; + } + model M extends N { + name: string; + va┆ + } + ` + ); + + check( + completions, + [ + { + label: "value", + insertText: "value", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nN.value: int16\n```", + }, + }, + { + label: "extra", + insertText: "extra", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nN.extra: boolean\n```", + }, + }, + ], + { + allowAdditionalCompletions: false, + } + ); + }); + it("completes template parameter uses", async () => { const completions = await complete( ` @@ -1027,6 +1112,1001 @@ describe("identifiers", () => { ]); }); + describe("completion for objectliteral/arrayliteral as template parameter default value", () => { + const def = ` + /** + * my log context + */ + model MyLogContext { + /** + * name of log context + */ + name: string; + /** + * items of context + */ + item: Array; + } + + /** + * my log argument + */ + model MyLogArg{ + /** + * my log message + */ + msg: string; + /** + * my log id + */ + id: int16; + /** + * my log context + */ + context: MyLogContext[]; + } + `; + + it("show all properties literal object, array, type", async () => { + ( + await Promise.all( + [ + `model TestModel{};`, + `model TestModel{};`, + `model TestModel{};`, + `model TestModel{};`, + `model TestModel{};`, + `model TestModel{};`, + ].map(async (item) => await complete(`${def}\n${item}`)) + ) + ).forEach((completions) => { + check( + completions, + [ + { + label: "msg", + insertText: "msg", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nMyLogArg.msg: string\n```\n\nmy log message", + }, + }, + { + label: "id", + insertText: "id", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nMyLogArg.id: int16\n```\n\nmy log id", + }, + }, + { + label: "context", + insertText: "context", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogArg.context: MyLogContext[]\n```\n\nmy log context", + }, + }, + ], + { + fullDocs: true, + allowAdditionalCompletions: false, + } + ); + }); + }); + + it("show all properties of literal model -> literal array -> literal model", async () => { + ( + await Promise.all( + [ + `model TestModel{};`, + `model TestModel{};`, + ].map(async (item) => await complete(`${def}\n${item}`)) + ) + ).forEach((completions) => { + check( + completions, + [ + { + label: "name", + insertText: "name", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogContext.name: string\n```\n\nname of log context", + }, + }, + { + label: "item", + insertText: "item", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogContext.item: Array\n```\n\nitems of context", + }, + }, + ], + { + fullDocs: true, + allowAdditionalCompletions: false, + } + ); + }); + }); + + it("no completion for type to value", async () => { + const completions = await complete( + `${def} + model TestModel{}; + ` + ); + ok(completions.items.length === 0, "No completions expected for model"); + }); + + it("no completion for value to type", async () => { + const completions = await complete( + `${def} + model TestModel{}; + ` + ); + ok(completions.items.length === 0, "No completions expected for model"); + }); + it("no completion when cursor is after }", async () => { + const completions = await complete( + `${def} + model TestModel{}; + ` + ); + ok(completions.items.length === 0, "No completions expected for model"); + }); + }); + + describe("completion for scalar init objectliteral/arrayliteral arg", () => { + const def = ` + /** + * my log context + */ + model MyLogContext { + /** + * name of log context + */ + name: string; + /** + * items of context + */ + item: Array; + } + + /** + * my log argument + */ + model MyLogArg{ + /** + * my log message + */ + msg: string; + /** + * my log id + */ + id: int16; + /** + * my log context + */ + context: MyLogContext[]; + } + + scalar TestString extends string{ + init createFromLog(value: MyLogArg); + init createFromLog2(value: MyLogArg[]); + init createFromLog3(value: string); + init createFromLog4(value1: int, value2: [{arg: [MyLogArg, string]}]) + } + `; + + it("show all properties literal model", async () => { + const completions = await complete( + `${def} + const c = TestString.createFromLog(#{┆}); + ` + ); + check( + completions, + [ + { + label: "msg", + insertText: "msg", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nMyLogArg.msg: string\n```\n\nmy log message", + }, + }, + { + label: "id", + insertText: "id", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nMyLogArg.id: int16\n```\n\nmy log id", + }, + }, + { + label: "context", + insertText: "context", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogArg.context: MyLogContext[]\n```\n\nmy log context", + }, + }, + ], + { + fullDocs: true, + allowAdditionalCompletions: false, + } + ); + }); + it("show all properties of literal array -> literal model", async () => { + const completions = await complete( + `${def} + const c = TestString.createFromLog2(#[#{┆}]); + ` + ); + check( + completions, + [ + { + label: "msg", + insertText: "msg", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nMyLogArg.msg: string\n```\n\nmy log message", + }, + }, + { + label: "id", + insertText: "id", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nMyLogArg.id: int16\n```\n\nmy log id", + }, + }, + { + label: "context", + insertText: "context", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogArg.context: MyLogContext[]\n```\n\nmy log context", + }, + }, + ], + { + fullDocs: true, + allowAdditionalCompletions: false, + } + ); + }); + it("show all properties of tuple->object->tuple->object", async () => { + const completions = await complete( + `${def} + const c = TestString.createFromLog4(1, #[#{arg:#[#{┆},"abc"]}]); + ` + ); + check( + completions, + [ + { + label: "msg", + insertText: "msg", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nMyLogArg.msg: string\n```\n\nmy log message", + }, + }, + { + label: "id", + insertText: "id", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nMyLogArg.id: int16\n```\n\nmy log id", + }, + }, + { + label: "context", + insertText: "context", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogArg.context: MyLogContext[]\n```\n\nmy log context", + }, + }, + ], + { + fullDocs: true, + allowAdditionalCompletions: false, + } + ); + }); + it("no completion for model", async () => { + const completions = await complete( + `${def} + const c = TestString.createFromLog({┆}); + ` + ); + ok(completions.items.length === 0, "No completions expected for model"); + }); + it("no completion for non-literalobject type", async () => { + const completions = await complete( + `${def} + const c = TestString.createFromLog3(┆); + ` + ); + ok(completions.items.length === 0, "No completions expected for model"); + }); + it("no completion when cursor is after }", async () => { + const completions = await complete( + `${def} + const c = TestString.createFromLog({}┆); + ` + ); + ok(completions.items.length === 0, "No completions expected for model"); + }); + }); + + describe("completion for const assignment of objectliteral/arrayliteral", () => { + const def = ` + /** + * my log context + */ + model MyLogContext { + /** + * name of log context + */ + name: string; + /** + * items of context + */ + item: Array; + } + + /** + * my log argument + */ + model MyLogArg{ + /** + * my log message + */ + msg: string; + /** + * my log id + */ + id: int16; + /** + * my log context + */ + context: MyLogContext[]; + /** + * my log context2 + */ + context2: [MyLogContext, int16]; + } + `; + it("show all properties literal model", async () => { + const completions = await complete( + `${def} + const c : MyLogArg = #{┆}; + ` + ); + check( + completions, + [ + { + label: "msg", + insertText: "msg", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nMyLogArg.msg: string\n```\n\nmy log message", + }, + }, + { + label: "id", + insertText: "id", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nMyLogArg.id: int16\n```\n\nmy log id", + }, + }, + { + label: "context", + insertText: "context", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogArg.context: MyLogContext[]\n```\n\nmy log context", + }, + }, + { + label: "context2", + insertText: "context2", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogArg.context2: [MyLogContext, int16]\n```\n\nmy log context2", + }, + }, + ], + { + fullDocs: true, + allowAdditionalCompletions: false, + } + ); + }); + + it("show all properties of literal array -> literal model", async () => { + const completions = await complete( + `${def} + const c : MyLogArg[] = #[#{┆}]; + ` + ); + check( + completions, + [ + { + label: "msg", + insertText: "msg", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nMyLogArg.msg: string\n```\n\nmy log message", + }, + }, + { + label: "id", + insertText: "id", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nMyLogArg.id: int16\n```\n\nmy log id", + }, + }, + { + label: "context", + insertText: "context", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogArg.context: MyLogContext[]\n```\n\nmy log context", + }, + }, + { + label: "context2", + insertText: "context2", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogArg.context2: [MyLogContext, int16]\n```\n\nmy log context2", + }, + }, + ], + { + fullDocs: true, + allowAdditionalCompletions: false, + } + ); + }); + + it("show all properties of literal model -> literal array -> literal model", async () => { + const completions = await complete( + `${def} + const c : MyLogArg = #{context:#[#{┆}]}; + ` + ); + check( + completions, + [ + { + label: "name", + insertText: "name", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogContext.name: string\n```\n\nname of log context", + }, + }, + { + label: "item", + insertText: "item", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogContext.item: Array\n```\n\nitems of context", + }, + }, + ], + { + fullDocs: true, + allowAdditionalCompletions: false, + } + ); + }); + + it("show all properties of literal model -> tuple -> literal model", async () => { + const completions = await complete( + `${def} + const c : MyLogArg = #{context2:#[#{┆}]}; + ` + ); + check( + completions, + [ + { + label: "name", + insertText: "name", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogContext.name: string\n```\n\nname of log context", + }, + }, + { + label: "item", + insertText: "item", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogContext.item: Array\n```\n\nitems of context", + }, + }, + ], + { + fullDocs: true, + allowAdditionalCompletions: false, + } + ); + }); + + it("show all properties of alias -> tuple -> literal model -> array -> literal model", async () => { + const completions = await complete( + `${def} + alias A = [MyLogArg]; + const c : A = #[#{context:#[#{┆}]}]; + ` + ); + check( + completions, + [ + { + label: "name", + insertText: "name", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogContext.name: string\n```\n\nname of log context", + }, + }, + { + label: "item", + insertText: "item", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogContext.item: Array\n```\n\nitems of context", + }, + }, + ], + { + fullDocs: true, + allowAdditionalCompletions: false, + } + ); + }); + + it("no completion for scalar array in literal object", async () => { + const completions = await complete( + `${def} + const c : MyLogArg = #{context:#[#{item: #[┆]}]}; + ` + ); + ok(completions.items.length === 0, "No completions expected for scalar array"); + }); + + it("no completion for model", async () => { + const completions = await complete( + `${def} + const c : MyLogArg = {┆}; + ` + ); + ok(completions.items.length === 0, "No completions expected for model"); + }); + + it("no completion when cursor is after }", async () => { + const completions = await complete( + `${def} + const c : MyLogArg = #{}┆; + ` + ); + ok(completions.items.length === 0, "No completions expected after }"); + }); + + it("no completion for const without type", async () => { + const completions = await complete( + `${def} + const c = #{┆}; + ` + ); + ok(completions.items.length === 0, "No completions expected for const without type"); + }); + }); + + describe("completion for decorator model/value argument", () => { + const decArgModelDef = ` + import "./decorators.js"; + + /** + * my log context + */ + model MyLogContext { + /** + * name of log context + */ + name: string; + /** + * items of context + */ + item: Record; + } + + /** + * my log argument + */ + model MyLogArg{ + /** + * my log message + */ + msg: string; + /** + * my log id + */ + id: int16; + /** + * my log context + */ + context: MyLogContext; + } + + extern dec myDec(target, arg: MyLogArg, arg2: valueof MyLogArg, arg3: [string, MyLogArg, int], arg4: valueof [MyLogArg]); + `; + + it("show all properties", async () => { + const js = { + name: "test/decorators.js", + js: { + $myDec: function () {}, + }, + }; + + ( + await Promise.all( + [ + `@myDec({┆})`, + `@myDec({}, #{┆})`, + `@myDec({}, {┆})`, + `@myDec({}, {}, ["abc", {┆}, 16])`, + `@myDec({}, {}, #[], #[#{┆}])`, + ].map(async (dec) => { + return await complete( + `${decArgModelDef} + ${dec} + model M {} + `, + js + ); + }) + ) + ).forEach((completions) => + check( + completions, + [ + { + label: "msg", + insertText: "msg", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nMyLogArg.msg: string\n```\n\nmy log message", + }, + }, + { + label: "id", + insertText: "id", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nMyLogArg.id: int16\n```\n\nmy log id", + }, + }, + { + label: "context", + insertText: "context", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogArg.context: MyLogContext\n```\n\nmy log context", + }, + }, + ], + { + fullDocs: true, + allowAdditionalCompletions: false, + } + ) + ); + + const result = await complete( + `${decArgModelDef} + @myDec(#{┆}) + model M {} + `, + js + ); + ok(result.items.length === 0, "No completions expected when value is used for type"); + }); + + it("show all properties of nested model", async () => { + const js = { + name: "test/decorators.js", + js: { + $myDec: function () {}, + }, + }; + + ( + await Promise.all( + [ + `@myDec({ context: {┆} })`, + `@myDec({ context: {} }, #{ context: #{┆} })`, + `@myDec({ context: {} }, { context: {┆} })`, + ].map(async (dec) => { + return await complete( + `${decArgModelDef} + ${dec} + model M {} + `, + js + ); + }) + ) + ).forEach((completions) => { + check( + completions, + [ + { + label: "name", + insertText: "name", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogContext.name: string\n```\n\nname of log context", + }, + }, + { + label: "item", + insertText: "item", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogContext.item: Record\n```\n\nitems of context", + }, + }, + ], + { + fullDocs: true, + allowAdditionalCompletions: false, + } + ); + }); + + const result = await complete( + `${decArgModelDef} + @myDec(#{ context: #{┆} }, { context: {} }) + model M {} + `, + js + ); + ok(result.items.length === 0, "No completions expected when value is used for type"); + }); + + it("show the left properties", async () => { + const js = { + name: "test/decorators.js", + js: { + $myDec: function () {}, + }, + }; + + ( + await Promise.all( + [ + `@myDec({ context: { name: "abc", ┆} })`, + `@myDec({}, #{ context: #{ name: "abc", ┆} })`, + `@myDec({}, { context: { name: "abc", ┆} })`, + ].map(async (dec) => { + return await complete( + `${decArgModelDef} + ${dec} + model M {} + `, + js + ); + }) + ) + ).forEach((completions) => { + check( + completions, + [ + { + label: "item", + insertText: "item", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogContext.item: Record\n```\n\nitems of context", + }, + }, + ], + { + fullDocs: true, + allowAdditionalCompletions: false, + } + ); + }); + const result = await complete( + `${decArgModelDef} + @myDec(#{ context: #{ name: "abc", ┆} }) + model M {} + `, + js + ); + ok(result.items.length === 0, "No completions expected when value is used for type"); + }); + + it("show the typing and left properties", async () => { + const js = { + name: "test/decorators.js", + js: { + $myDec: function () {}, + }, + }; + + ( + await Promise.all( + [ + `@myDec({ msg: "msg", conte┆xt})`, + `@myDec({}, { msg: "msg", conte┆xt})`, + `@myDec({}, #{ msg: "msg", conte┆xt})`, + ].map(async (dec) => { + return await complete( + `${decArgModelDef} + ${dec} + model M {} + `, + js + ); + }) + ) + ).forEach((completions) => + check( + completions, + [ + { + label: "id", + insertText: "id", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nMyLogArg.id: int16\n```\n\nmy log id", + }, + }, + { + label: "context", + insertText: "context", + kind: CompletionItemKind.Field, + documentation: { + kind: MarkupKind.Markdown, + value: + "(model property)\n```typespec\nMyLogArg.context: MyLogContext\n```\n\nmy log context", + }, + }, + ], + { + fullDocs: true, + allowAdditionalCompletions: false, + } + ) + ); + const result = await complete( + `${decArgModelDef} + @myDec(#{ msg: "msg", conte┆xt}) + model M {} + `, + js + ); + ok(result.items.length === 0, "No completions expected when value is used for type"); + }); + + it("no completion when cursor is after }", async () => { + const js = { + name: "test/decorators.js", + js: { + $myDec: function () {}, + }, + }; + + const completions = await complete( + `${decArgModelDef} + @myDec({}┆) + model M {} + `, + js + ); + ok(completions.items.length === 0, "No completions expected when cursor is after }"); + }); + + it("no completion when the model expression is not decorator argument value", async () => { + const js = { + name: "test/decorators.js", + js: { + $myDec: function () {}, + }, + }; + + const completions = await complete( + `${decArgModelDef} + @myDec({}) + model M {} + + op op1() : { + na┆me: string; + value: string + } + `, + js + ); + ok(completions.items.length === 0, "No completions expected for normal model expression }"); + }); + }); + describe("directives", () => { it("complete directives when starting with `#`", async () => { const completions = await complete( diff --git a/packages/compiler/test/server/get-hover.test.ts b/packages/compiler/test/server/get-hover.test.ts index 0594668a4c..cb5dc4ffe7 100644 --- a/packages/compiler/test/server/get-hover.test.ts +++ b/packages/compiler/test/server/get-hover.test.ts @@ -33,6 +33,27 @@ describe("compiler: server: on hover", () => { }, }); }); + + it("scalar init with object literal argument", async () => { + const hover = await getHoverAtCursor(` + model MyModel { + /** + * name of the model + */ + name: string; + } + scalar MyString extends string{ + init createFromModel(arg: MyModel); + } + const abc = MyString.createFromModel(#{ na┆me: "hello" }); + `); + deepStrictEqual(hover, { + contents: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nMyModel.name: string\n```\n\nname of the model", + }, + }); + }); }); describe("enum", () => { @@ -135,6 +156,107 @@ describe("compiler: server: on hover", () => { }, }); }); + + const decArgModelDef = ` + import "./dec-types.js"; + + /** + * my log context + */ + model MyLogContext { + /** + * name of log context + */ + name: string; + /** + * items of context + */ + item: Record; + } + + /** + * my log argument + */ + model MyLogArg{ + /** + * my log message + */ + msg: string; + /** + * my log id + */ + id: int16; + /** + * my log context + */ + context: MyLogContext; + } + + extern dec single(target, arg: MyLogArg);`; + + it("test model expression as decorator parameter value", async () => { + const hover = await getHoverAtCursor( + ` + ${decArgModelDef} + @single({ + ms┆g: "hello", + id: 1, + context: { + name: "my context", + item: { + key: "value" + } + } + + }) + namespace TestNS; + ` + ); + deepStrictEqual(hover, { + contents: { + kind: MarkupKind.Markdown, + value: + "(model property)\n" + + "```typespec\n" + + "MyLogArg.msg: string\n" + + "```\n" + + "\n" + + "my log message", + }, + }); + }); + + it("test nested model expression as decorator parameter value", async () => { + const hover = await getHoverAtCursor( + ` + ${decArgModelDef} + @single({ + msg: "hello", + id: 1, + context: { + name: "my context", + it┆em: { + key: "value" + } + } + + }) + namespace TestNS; + ` + ); + deepStrictEqual(hover, { + contents: { + kind: MarkupKind.Markdown, + value: + "(model property)\n" + + "```typespec\n" + + "MyLogContext.item: Record\n" + + "```\n" + + "\n" + + "items of context", + }, + }); + }); }); describe("namespace", () => { @@ -477,6 +599,26 @@ describe("compiler: server: on hover", () => { }, }); }); + + it("object literal property", async () => { + const hover = await getHoverAtCursor( + ` + model MyModel { + /** + * name of the model + */ + name: string; + } + const abc : MyModel = #{ na┆me: "hello" }; + ` + ); + deepStrictEqual(hover, { + contents: { + kind: MarkupKind.Markdown, + value: "(model property)\n```typespec\nMyModel.name: string\n```\n\nname of the model", + }, + }); + }); }); async function getHoverAtCursor(sourceWithCursor: string): Promise { diff --git a/packages/compiler/test/server/misc.test.ts b/packages/compiler/test/server/misc.test.ts index ab28a5c450..8ff5218b52 100644 --- a/packages/compiler/test/server/misc.test.ts +++ b/packages/compiler/test/server/misc.test.ts @@ -13,7 +13,7 @@ describe("compiler: server: misc", () => { const { source, pos } = extractCursor(sourceWithCursor); const root = parse(source); dumpAST(root); - return { node: getCompletionNodeAtPosition(root, pos), root }; + return { node: getCompletionNodeAtPosition(root, pos)?.node, root }; } it("return identifier for property return type", async () => {