From 92cf2eeb174a2ca341cd90282502b91c23f3347b Mon Sep 17 00:00:00 2001 From: RodgeFu Date: Thu, 23 May 2024 23:14:43 +0800 Subject: [PATCH 1/4] support keyword extends/is --- packages/compiler/src/core/charcode.ts | 13 +- packages/compiler/src/core/parser.ts | 292 +++++++++++++----- packages/compiler/src/core/scanner.ts | 65 +++- packages/compiler/src/core/types.ts | 48 ++- packages/compiler/src/server/completion.ts | 183 +++++++++-- packages/compiler/src/server/serverlib.ts | 21 +- .../compiler/test/server/completion.test.ts | 111 ++++++- packages/compiler/test/server/misc.test.ts | 26 +- 8 files changed, 616 insertions(+), 143 deletions(-) diff --git a/packages/compiler/src/core/charcode.ts b/packages/compiler/src/core/charcode.ts index c493a08078..3a06f2e32a 100644 --- a/packages/compiler/src/core/charcode.ts +++ b/packages/compiler/src/core/charcode.ts @@ -247,17 +247,20 @@ export function isNonAsciiIdentifierCharacter(codePoint: number) { return lookupInNonAsciiMap(codePoint, nonAsciiIdentifierMap); } -export function codePointBefore(text: string, pos: number): number | undefined { - if (pos <= 0 || pos >= text.length) { - return undefined; +export function codePointBefore( + text: string, + pos: number +): { char: number | undefined; size: number } { + if (pos <= 0 || pos > text.length) { + return { char: undefined, size: 0 }; } const ch = text.charCodeAt(pos - 1); if (!isLowSurrogate(ch) || !isHighSurrogate(text.charCodeAt(pos - 2))) { - return ch; + return { char: ch, size: 1 }; } - return text.codePointAt(pos - 2); + return { char: text.codePointAt(pos - 2), size: 2 }; } function lookupInNonAsciiMap(codePoint: number, map: readonly number[]) { diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 12dcc2cf19..e390413362 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -1,5 +1,5 @@ import { isArray, mutate } from "../utils/misc.js"; -import { trim } from "./charcode.js"; +import { codePointBefore, isIdentifierContinue, trim } from "./charcode.js"; import { compilerAssert } from "./diagnostics.js"; import { CompilerDiagnostics, createDiagnostic } from "./messages.js"; import { @@ -12,6 +12,9 @@ import { isPunctuation, isStatementKeyword, isTrivia, + skipContinuousIdentifier, + skipTrivia, + skipTriviaBackward, } from "./scanner.js"; import { AliasStatementNode, @@ -134,6 +137,17 @@ type ParseListItem = K extends UnannotatedListKind ? () => T : (pos: number, decorators: DecoratorExpressionNode[]) => T; +type ListDetail = { + items: T[]; + /** + * The range of the list items as below as an example + * model Foo { a: string; b: string; } + * + * remark: if the start/end token (i.e. { } ) not found, pos/end will be -1 + */ + range: TextRange; +}; + type OpenToken = | Token.OpenBrace | Token.OpenParen @@ -700,9 +714,10 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ): InterfaceStatementNode { parseExpected(Token.InterfaceKeyword); const id = parseIdentifier(); - const templateParameters = parseTemplateParameterList(); + const { items: templateParameters, range: templateParametersRange } = + parseTemplateParameterList(); - let extendList: TypeReferenceNode[] = []; + let extendList: ListDetail = createEmptyList(); if (token() === Token.ExtendsKeyword) { nextToken(); extendList = parseList(ListKind.Heritage, parseReferenceExpression); @@ -711,25 +726,28 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa nextToken(); } - const operations = parseList(ListKind.InterfaceMembers, (pos, decorators) => - parseOperationStatement(pos, decorators, true) + const { items: operations, range: operationsRange } = parseList( + ListKind.InterfaceMembers, + (pos, decorators) => parseOperationStatement(pos, decorators, true) ); return { kind: SyntaxKind.InterfaceStatement, id, templateParameters, + templateParametersRange, operations, - extends: extendList, + operationsRange, + extends: extendList.items, decorators, ...finishNode(pos), }; } - function parseTemplateParameterList(): TemplateParameterDeclarationNode[] { - const list = parseOptionalList(ListKind.TemplateParameters, parseTemplateParameter); + function parseTemplateParameterList(): ListDetail { + const detail = parseOptionalList(ListKind.TemplateParameters, parseTemplateParameter); let setDefault = false; - for (const item of list) { + for (const item of detail.items) { if (!item.default && setDefault) { error({ code: "default-required", target: item }); continue; @@ -740,7 +758,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa } } - return list; + return detail; } function parseUnionStatement( @@ -749,14 +767,16 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ): UnionStatementNode { parseExpected(Token.UnionKeyword); const id = parseIdentifier(); - const templateParameters = parseTemplateParameterList(); + const { items: templateParameters, range: templateParametersRange } = + parseTemplateParameterList(); - const options = parseList(ListKind.UnionVariants, parseUnionVariant); + const { items: options } = parseList(ListKind.UnionVariants, parseUnionVariant); return { kind: SyntaxKind.UnionStatement, id, templateParameters, + templateParametersRange, decorators, options, ...finishNode(pos), @@ -833,7 +853,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa } const id = parseIdentifier(); - const templateParameters = parseTemplateParameterList(); + const { items: templateParameters, range: templateParametersRange } = + parseTemplateParameterList(); // Make sure the next token is one that is expected const token = expectTokenIsOneOf(Token.OpenParen, Token.IsKeyword); @@ -872,6 +893,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa kind: SyntaxKind.OperationStatement, id, templateParameters, + templateParametersRange, signature, decorators, ...finishNode(pos), @@ -880,10 +902,14 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseOperationParameters(): ModelExpressionNode { const pos = tokenPos(); - const properties = parseList(ListKind.OperationParameters, parseModelPropertyOrSpread); + const { items: properties, range: propertiesRange } = parseList( + ListKind.OperationParameters, + parseModelPropertyOrSpread + ); const parameters: ModelExpressionNode = { kind: SyntaxKind.ModelExpression, properties, + propertiesRange, ...finishNode(pos), }; return parameters; @@ -895,23 +921,26 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ): ModelStatementNode { parseExpected(Token.ModelKeyword); const id = parseIdentifier(); - const templateParameters = parseTemplateParameterList(); + const { items: templateParameters, range: templateParametersRange } = + parseTemplateParameterList(); expectTokenIsOneOf(Token.OpenBrace, Token.Equals, Token.ExtendsKeyword, Token.IsKeyword); const optionalExtends = parseOptionalModelExtends(); const optionalIs = optionalExtends ? undefined : parseOptionalModelIs(); - let properties: (ModelPropertyNode | ModelSpreadPropertyNode)[] = []; + let propDetail: ListDetail = createEmptyList< + ModelPropertyNode | ModelSpreadPropertyNode + >(); if (optionalIs) { const tok = expectTokenIsOneOf(Token.Semicolon, Token.OpenBrace); if (tok === Token.Semicolon) { nextToken(); } else { - properties = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread); + propDetail = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread); } } else { - properties = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread); + propDetail = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread); } return { @@ -920,8 +949,10 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa extends: optionalExtends, is: optionalIs, templateParameters, + templateParametersRange, decorators, - properties, + properties: propDetail.items, + propertiesRange: propDetail.range, ...finishNode(pos), }; } @@ -1090,17 +1121,20 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ): ScalarStatementNode { parseExpected(Token.ScalarKeyword); const id = parseIdentifier(); - const templateParameters = parseTemplateParameterList(); + const { items: templateParameters, range: templateParametersRange } = + parseTemplateParameterList(); const optionalExtends = parseOptionalScalarExtends(); - const members = parseScalarMembers(); + const { items: members, range: membersRange } = parseScalarMembers(); return { kind: SyntaxKind.ScalarStatement, id, templateParameters, + templateParametersRange, extends: optionalExtends, members, + membersRange, decorators, ...finishNode(pos), }; @@ -1113,10 +1147,10 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa return undefined; } - function parseScalarMembers(): readonly ScalarConstructorNode[] { + function parseScalarMembers(): ListDetail { if (token() === Token.Semicolon) { nextToken(); - return []; + return createEmptyList(); } else { return parseList(ListKind.ScalarMembers, parseScalarMember); } @@ -1130,11 +1164,12 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa parseExpected(Token.InitKeyword); const id = parseIdentifier(); - const parameters = parseFunctionParameters(); + const { items: parameters, range: parametersRange } = parseFunctionParameters(); return { kind: SyntaxKind.ScalarConstructor, id, parameters, + parametersRange, ...finishNode(pos), }; } @@ -1145,12 +1180,16 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ): EnumStatementNode { parseExpected(Token.EnumKeyword); const id = parseIdentifier(); - const members = parseList(ListKind.EnumMembers, parseEnumMemberOrSpread); + const { items: members, range: membersRange } = parseList( + ListKind.EnumMembers, + parseEnumMemberOrSpread + ); return { kind: SyntaxKind.EnumStatement, id, decorators, members, + membersRange, ...finishNode(pos), }; } @@ -1212,7 +1251,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseAliasStatement(pos: number): AliasStatementNode { parseExpected(Token.AliasKeyword); const id = parseIdentifier(); - const templateParameters = parseTemplateParameterList(); + const { items: templateParameters, range: templateParametersRange } = + parseTemplateParameterList(); parseExpected(Token.Equals); const value = parseExpression(); parseExpected(Token.Semicolon); @@ -1220,6 +1260,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa kind: SyntaxKind.AliasStatement, id, templateParameters, + templateParametersRange, value, ...finishNode(pos), }; @@ -1386,10 +1427,15 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const pos = tokenPos(); const target = parseIdentifierOrMemberExpression(message); if (token() === Token.OpenParen) { + const { items: args, range: argumentsRange } = parseList( + ListKind.FunctionArguments, + parseExpression + ); return { kind: SyntaxKind.CallExpression, target, - arguments: parseList(ListKind.FunctionArguments, parseExpression), + arguments: args, + argumentsRange, ...finishNode(pos), }; } @@ -1401,12 +1447,16 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa target: IdentifierNode | MemberExpressionNode, pos: number ): TypeReferenceNode { - const args = parseOptionalList(ListKind.TemplateArguments, parseTemplateArgument); + const { items: args, range: argumentsRange } = parseOptionalList( + ListKind.TemplateArguments, + parseTemplateArgument + ); return { kind: SyntaxKind.TypeReference, target, arguments: args, + argumentsRange, ...finishNode(pos), }; } @@ -1461,29 +1511,37 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa // `@` applied to `model Foo`, and not as `@model` // applied to invalid statement `Foo`. const target = parseIdentifierOrMemberExpression(undefined, false); - const args = parseOptionalList(ListKind.DecoratorArguments, parseExpression); + const { items: args, range: argRange } = parseOptionalList( + ListKind.DecoratorArguments, + parseExpression + ); if (args.length === 0) { error({ code: "augment-decorator-target" }); + const emptyList = createEmptyList(); return { kind: SyntaxKind.AugmentDecoratorStatement, target, targetType: { kind: SyntaxKind.TypeReference, target: createMissingIdentifier(), - arguments: [], + arguments: emptyList.items, + argumentsRange: emptyList.range, ...finishNode(pos), }, - arguments: [], + arguments: args, + allArgumentsRange: argRange, ...finishNode(pos), }; } let [targetEntity, ...decoratorArgs] = args; if (targetEntity.kind !== SyntaxKind.TypeReference) { error({ code: "augment-decorator-target", target: targetEntity }); + const emptyList = createEmptyList(); targetEntity = { kind: SyntaxKind.TypeReference, target: createMissingIdentifier(), - arguments: [], + arguments: emptyList.items, + argumentsRange: emptyList.range, ...finishNode(pos), }; } @@ -1495,6 +1553,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa target, targetType: targetEntity, arguments: decoratorArgs, + allArgumentsRange: argRange, ...finishNode(pos), }; } @@ -1521,10 +1580,14 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa // `@` applied to `model Foo`, and not as `@model` // applied to invalid statement `Foo`. const target = parseIdentifierOrMemberExpression(undefined, false); - const args = parseOptionalList(ListKind.DecoratorArguments, parseExpression); + const { items: args, range: argsRange } = parseOptionalList( + ListKind.DecoratorArguments, + parseExpression + ); return { kind: SyntaxKind.DecoratorExpression, arguments: args, + argumentsRange: argsRange, target, ...finishNode(pos), }; @@ -1721,43 +1784,50 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseTupleExpression(): TupleExpressionNode { const pos = tokenPos(); - const values = parseList(ListKind.Tuple, parseExpression); + const { items: values, range: valuesRange } = parseList(ListKind.Tuple, parseExpression); return { kind: SyntaxKind.TupleExpression, values, + valuesRange, ...finishNode(pos), }; } function parseModelExpression(): ModelExpressionNode { const pos = tokenPos(); - const properties = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread); + const { items: properties, range: propertiesRange } = parseList( + ListKind.ModelProperties, + parseModelPropertyOrSpread + ); return { kind: SyntaxKind.ModelExpression, properties, + propertiesRange, ...finishNode(pos), }; } function parseObjectLiteral(): ObjectLiteralNode { const pos = tokenPos(); - const properties = parseList( + const { items: properties, range: propertiesRange } = parseList( ListKind.ObjectLiteralProperties, parseObjectLiteralPropertyOrSpread ); return { kind: SyntaxKind.ObjectLiteral, properties, + propertiesRange, ...finishNode(pos), }; } function parseArrayLiteral(): ArrayLiteralNode { const pos = tokenPos(); - const values = parseList(ListKind.ArrayLiteral, parseExpression); + const { items: values, range: valuesRange } = parseList(ListKind.ArrayLiteral, parseExpression); return { kind: SyntaxKind.ArrayLiteral, values, + valuesRange, ...finishNode(pos), }; } @@ -1977,7 +2047,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const modifierFlags = modifiersToFlags(modifiers); parseExpected(Token.DecKeyword); const id = parseIdentifier(); - let [target, ...parameters] = parseFunctionParameters(); + const allParamListDetail = parseFunctionParameters(); + let [target, ...parameters] = allParamListDetail.items; if (target === undefined) { error({ code: "decorator-decl-target", target: { pos, end: previousTokenEnd } }); target = { @@ -2000,6 +2071,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa id, target, parameters, + allParametersRange: allParamListDetail.range, ...finishNode(pos), }; } @@ -2011,7 +2083,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const modifierFlags = modifiersToFlags(modifiers); parseExpected(Token.FnKeyword); const id = parseIdentifier(); - const parameters = parseFunctionParameters(); + const { items: parameters, range: parametersRange } = parseFunctionParameters(); let returnType; if (parseOptional(Token.Colon)) { returnType = parseExpression(); @@ -2023,19 +2095,20 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa modifierFlags, id, parameters, + parametersRange, returnType, ...finishNode(pos), }; } - function parseFunctionParameters(): FunctionParameterNode[] { + function parseFunctionParameters(): ListDetail { const parameters = parseList( ListKind.FunctionParameters, parseFunctionParameter ); let foundOptional = false; - for (const [index, item] of parameters.entries()) { + for (const [index, item] of parameters.items.entries()) { if (!item.optional && foundOptional) { error({ code: "required-parameter-first", target: item }); continue; @@ -2048,7 +2121,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa if (item.rest && item.optional) { error({ code: "rest-parameter-required", target: item }); } - if (item.rest && index !== parameters.length - 1) { + if (item.rest && index !== parameters.items.length - 1) { error({ code: "rest-parameter-last", target: item }); } } @@ -2148,7 +2221,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa let parameters: ProjectionParameterDeclarationNode[]; if (token() === Token.OpenParen) { - parameters = parseList(ListKind.ProjectionParameter, parseProjectionParameter); + parameters = parseList(ListKind.ProjectionParameter, parseProjectionParameter).items; } else { parameters = []; } @@ -2393,7 +2466,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa kind: SyntaxKind.ProjectionCallExpression, callKind: "method", target: expr, - arguments: parseList(ListKind.CallArguments, parseProjectionExpression), + arguments: parseList(ListKind.CallArguments, parseProjectionExpression).items, ...finishNode(pos), }; } else { @@ -2484,7 +2557,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseProjectionLambdaOrParenthesizedExpression(): ProjectionExpression { const pos = tokenPos(); - const exprs = parseList(ListKind.ProjectionExpression, parseProjectionExpression); + const exprs = parseList(ListKind.ProjectionExpression, parseProjectionExpression).items; if (token() === Token.EqualsGreaterThan) { // unpack the exprs (which should be just identifiers) into a param list const params: ProjectionLambdaParameterDeclarationNode[] = []; @@ -2542,7 +2615,10 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseProjectionModelExpression(): ProjectionModelExpressionNode { const pos = tokenPos(); - const properties = parseList(ListKind.ModelProperties, parseProjectionModelPropertyOrSpread); + const { items: properties } = parseList( + ListKind.ModelProperties, + parseProjectionModelPropertyOrSpread + ); return { kind: SyntaxKind.ProjectionModelExpression, properties, @@ -2636,7 +2712,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseProjectionTupleExpression(): ProjectionTupleExpressionNode { const pos = tokenPos(); - const values = parseList(ListKind.Tuple, parseProjectionExpression); + const { items: values } = parseList(ListKind.Tuple, parseProjectionExpression); return { kind: SyntaxKind.ProjectionTupleExpression, values, @@ -3041,11 +3117,13 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function createMissingTypeReference(): TypeReferenceNode { const pos = tokenPos(); + const { items: args, range: argsRange } = createEmptyList(); return { kind: SyntaxKind.TypeReference, target: createMissingIdentifier(), - arguments: [], + arguments: args, + argumentsRange: argsRange, ...finishNode(pos), }; } @@ -3061,6 +3139,13 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa return obj as any; } + function createEmptyList(range: TextRange = { pos: -1, end: -1 }): ListDetail { + return { + items: [], + range, + }; + } + /** * Parse a delimited list of elements, including the surrounding open and * close punctuation @@ -3079,16 +3164,20 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseList( kind: K, parseItem: ParseListItem - ): T[] { + ): ListDetail { + const r: ListDetail = createEmptyList(); if (kind.open !== Token.None) { - parseExpected(kind.open); + const t = tokenPos(); + if (parseExpected(kind.open)) { + mutate(r.range).pos = t; + } } if (kind.allowEmpty && parseOptional(kind.close)) { - return []; + mutate(r.range).end = previousTokenEnd; + return r; } - const items: T[] = []; while (true) { const startingPos = tokenPos(); const { pos, docs, directives, decorators } = parseAnnotations({ @@ -3104,7 +3193,9 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa // of file. Note, however, that we must parse a missing element if // there were directives or decorators as we cannot drop those from // the tree. - parseExpected(kind.close); + if (parseExpected(kind.close)) { + mutate(r.range).end = previousTokenEnd; + } break; } @@ -3117,13 +3208,14 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa mutate(item).directives = directives; } - items.push(item); + r.items.push(item); const delimiter = token(); const delimiterPos = tokenPos(); if (parseOptionalDelimiter(kind)) { // Delimiter found: check if it's trailing. if (parseOptional(kind.close)) { + mutate(r.range).end = previousTokenEnd; if (!kind.trailingDelimiterIsValid) { error({ code: "trailing-token", @@ -3145,6 +3237,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa // there's no delimiter after an item. break; } else if (parseOptional(kind.close)) { + mutate(r.range).end = previousTokenEnd; // If a list *is* surrounded by punctuation, then the list ends when we // reach the close token. break; @@ -3154,7 +3247,9 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa // assumption that the closing delimiter is missing. This check is // duplicated from above to preempt the parseExpected(delimeter) // below. - parseExpected(kind.close); + if (parseExpected(kind.close)) { + mutate(r.range).end = previousTokenEnd; + } break; } else { // Error recovery: if a list kind *is* surrounded by punctuation and we @@ -3173,16 +3268,17 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa // // Simple repro: `model M { ]` would loop forever without this check. // - parseExpected(kind.close); + if (parseExpected(kind.close)) { + mutate(r.range).end = previousTokenEnd; + } nextToken(); // remove the item that was entirely inserted by error recovery. - items.pop(); + r.items.pop(); break; } } - - return items; + return r; } /** @@ -3192,8 +3288,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseOptionalList( kind: K, parseItem: ParseListItem - ): T[] { - return token() === kind.open ? parseList(kind, parseItem) : []; + ): ListDetail { + return token() === kind.open ? parseList(kind, parseItem) : createEmptyList(); } function parseOptionalDelimiter(kind: ListKind) { @@ -3713,25 +3809,68 @@ function visitEach(cb: NodeCallback, nodes: readonly Node[] | undefined): return; } +/** + * check whether a position belongs to a range (excluding the start and end pos) + * i.e. {...} + * + * remark: if range.pos is -1 means no start point found, so return false + * if range.end is -1 means no end point found, so return true if position is greater than range.pos + */ +export function positionInRange(position: number, range: TextRange) { + return range.pos >= 0 && position > range.pos && (range.end === -1 || position < range.end); +} + export function getNodeAtPositionDetail( script: TypeSpecScriptNode, position: number, - filter?: (node: Node) => boolean -): PositionDetail | undefined { - const node = getNodeAtPosition(script, position, filter); - if (!node) return undefined; + filter: (node: Node, flag: "cur" | "pre" | "post") => boolean = () => true +): PositionDetail { + const cur = getNodeAtPosition(script, position, (n) => filter(n, "cur")); + + const input = script.file.text; + const char = input.charCodeAt(position); + const preChar = position >= 0 ? input.charCodeAt(position - 1) : NaN; + const nextChar = position < input.length ? input.charCodeAt(position + 1) : NaN; - 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; + let inTrivia = false; + let triviaStart: number | undefined; + let triviaEnd: number | undefined; + if (!cur || cur.kind !== SyntaxKind.StringLiteral) { + const { char: cp } = codePointBefore(input, position); + if (!cp || !isIdentifierContinue(cp)) { + triviaEnd = skipTrivia(input, position); + triviaStart = skipTriviaBackward(script, position) + 1; + inTrivia = triviaEnd !== position; + } + } + + if (!inTrivia) { + const beforeId = skipContinuousIdentifier(input, position, true /*isBackward*/); + triviaStart = skipTriviaBackward(script, beforeId) + 1; + const afterId = skipContinuousIdentifier(input, position, false /*isBackward*/); + triviaEnd = skipTrivia(input, afterId); + } + + if (triviaStart === undefined || triviaEnd === undefined) { + compilerAssert(false, "unexpected, triviaStart and triviaEnd should be defined"); + } return { - node, - position, + node: cur, + char, preChar, nextChar, - char, + position, + inTrivia, + triviaStartPosition: triviaStart, + triviaEndPosition: triviaEnd, + getPositionDetailBeforeTrivia: () => { + // getNodeAtPosition will also include the 'node.end' position which is the triviaStart pos + return getNodeAtPositionDetail(script, triviaStart, (n) => filter(n, "pre")); + }, + getPositionDetailAfterTrivia: () => { + return getNodeAtPositionDetail(script, triviaEnd, (n) => filter(n, "post")); + }, }; } @@ -3836,7 +3975,14 @@ function isBlocklessNamespace(node: Node) { return node.statements === undefined; } -export function getFirstAncestor(node: Node, test: NodeCallback): Node | undefined { +export function getFirstAncestor( + node: Node, + test: NodeCallback, + includeSelf: boolean = false +): Node | undefined { + if (includeSelf && test(node)) { + return node; + } for (let n = node.parent; n; n = n.parent) { if (test(n)) { return n; diff --git a/packages/compiler/src/core/scanner.ts b/packages/compiler/src/core/scanner.ts index 74fd4d5528..1fb89aa186 100644 --- a/packages/compiler/src/core/scanner.ts +++ b/packages/compiler/src/core/scanner.ts @@ -1,5 +1,6 @@ import { CharCode, + codePointBefore, isAsciiIdentifierContinue, isAsciiIdentifierStart, isBinaryDigit, @@ -17,8 +18,9 @@ import { } from "./charcode.js"; import { DiagnosticHandler, compilerAssert } from "./diagnostics.js"; import { CompilerDiagnostics, createDiagnostic } from "./messages.js"; +import { getCommentAtPosition } from "./parser-utils.js"; import { createSourceFile } from "./source-file.js"; -import { DiagnosticReport, SourceFile, TextRange } from "./types.js"; +import { DiagnosticReport, SourceFile, TextRange, TypeSpecScriptNode } from "./types.js"; // All conflict markers consist of the same character repeated seven times. If it is // a <<<<<<< or >>>>>>> marker then it is also followed by a space. @@ -1372,7 +1374,54 @@ export function createScanner( } } +/** + * + * @param script + * @param position + * @param endPosition exclude + * @returns return === endPosition (or -1) means not found non-trivia until endPosition + 1 + */ +export function skipTriviaBackward( + script: TypeSpecScriptNode, + position: number, + endPosition = -1 +): number { + endPosition = endPosition < -1 ? -1 : endPosition; + const input = script.file.text; + if (position === input.length) { + // it's possible if the pos is at the end of the file, just treat it as trivia + position--; + } else if (position > input.length) { + compilerAssert(false, "position out of range"); + } + + while (position > endPosition) { + const ch = input.charCodeAt(position); + + if (isWhiteSpace(ch)) { + position--; + } else { + const comment = getCommentAtPosition(script, position); + if (comment) { + position = comment.pos - 1; + } else { + break; + } + } + } + + return position; +} + +/** + * + * @param input + * @param position + * @param endPosition exclude + * @returns return === endPosition (or input.length) means not found non-trivia until endPosition - 1 + */ export function skipTrivia(input: string, position: number, endPosition = input.length): number { + endPosition = endPosition > input.length ? input.length : endPosition; while (position < endPosition) { const ch = input.charCodeAt(position); @@ -1450,6 +1499,20 @@ function skipMultiLineComment( return [position, false]; } +export function skipContinuousIdentifier(input: string, position: number, isBackward = false) { + let cur = position; + const direction = isBackward ? -1 : 1; + const bar = isBackward ? (p: number) => p >= 0 : (p: number) => p < input.length; + while (bar(cur)) { + const { char: cp, size } = codePointBefore(input, cur); + cur += direction * size; + if (!cp || !isIdentifierContinue(cp)) { + break; + } + } + return cur; +} + function isConflictMarker(input: string, position: number, endPosition = input.length): boolean { // Conflict markers must be at the start of a line. const ch = input.charCodeAt(position); diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index acce9553f9..dc3628f02d 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -1043,6 +1043,7 @@ export interface BaseNode extends TextRange { export interface TemplateDeclarationNode { readonly templateParameters: readonly TemplateParameterDeclarationNode[]; + readonly templateParametersRange: TextRange; readonly locals?: SymbolTable; } @@ -1050,11 +1051,35 @@ export interface TemplateDeclarationNode { * owner node and other related information according to the position */ export interface PositionDetail { - readonly node: Node; + readonly node: Node | undefined; readonly position: number; readonly char: number; readonly preChar: number; readonly nextChar: number; + readonly inTrivia: boolean; + + /** + * if the position is in a trivia, return the start position of the trivia containing the position + * if the position is not a trivia, return the start position of the trivia before the text(identifier code) containing the position + * + * Please be aware that this may not be the pre node in the tree because some non-trivia char is ignored in the tree but will counted here + * + * also comments are considered as trivia + */ + readonly triviaStartPosition: number; + /** + * if the position is in a trivia, return the end position (exclude as other 'end' means) of the trivia containing the position + * if the position is not a trivia, return the end position (exclude as other 'end' means) of the trivia after the node containing the position + * + * Please be aware that this may not be the next node in the tree because some non-trivia char is ignored in the tree but will considered here + * + * also comments are considered as trivia + */ + readonly triviaEndPosition: number; + /** get the PositionDetail of positionBeforeTrivia */ + readonly getPositionDetailBeforeTrivia: () => PositionDetail; + /** get the PositionDetail of positionAfterTrivia */ + readonly getPositionDetailAfterTrivia: () => PositionDetail; } export type Node = @@ -1243,6 +1268,7 @@ export interface DecoratorExpressionNode extends BaseNode { readonly kind: SyntaxKind.DecoratorExpression; readonly target: IdentifierNode | MemberExpressionNode; readonly arguments: readonly Expression[]; + readonly argumentsRange: TextRange; } export interface AugmentDecoratorStatementNode extends BaseNode { @@ -1250,6 +1276,10 @@ export interface AugmentDecoratorStatementNode extends BaseNode { readonly target: IdentifierNode | MemberExpressionNode; readonly targetType: TypeReferenceNode; readonly arguments: readonly Expression[]; + /** + * range including targetType and arguments + */ + readonly allArgumentsRange: TextRange; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; } @@ -1358,6 +1388,7 @@ export interface OperationStatementNode extends BaseNode, DeclarationNode, Templ export interface ModelStatementNode extends BaseNode, DeclarationNode, TemplateDeclarationNode { readonly kind: SyntaxKind.ModelStatement; readonly properties: readonly (ModelPropertyNode | ModelSpreadPropertyNode)[]; + readonly propertiesRange: TextRange; readonly extends?: Expression; readonly is?: Expression; readonly decorators: readonly DecoratorExpressionNode[]; @@ -1369,6 +1400,7 @@ export interface ScalarStatementNode extends BaseNode, DeclarationNode, Template readonly extends?: TypeReferenceNode; readonly decorators: readonly DecoratorExpressionNode[]; readonly members: readonly ScalarConstructorNode[]; + readonly membersRange: TextRange; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; } @@ -1376,12 +1408,14 @@ export interface ScalarConstructorNode extends BaseNode { readonly kind: SyntaxKind.ScalarConstructor; readonly id: IdentifierNode; readonly parameters: FunctionParameterNode[]; + readonly parametersRange: TextRange; readonly parent?: ScalarStatementNode; } export interface InterfaceStatementNode extends BaseNode, DeclarationNode, TemplateDeclarationNode { readonly kind: SyntaxKind.InterfaceStatement; readonly operations: readonly OperationStatementNode[]; + readonly operationsRange: TextRange; readonly extends: readonly TypeReferenceNode[]; readonly decorators: readonly DecoratorExpressionNode[]; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; @@ -1405,6 +1439,7 @@ export interface UnionVariantNode extends BaseNode { export interface EnumStatementNode extends BaseNode, DeclarationNode { readonly kind: SyntaxKind.EnumStatement; readonly members: readonly (EnumMemberNode | EnumSpreadMemberNode)[]; + readonly membersRange: TextRange; readonly decorators: readonly DecoratorExpressionNode[]; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; } @@ -1438,6 +1473,7 @@ export interface CallExpressionNode extends BaseNode { readonly kind: SyntaxKind.CallExpression; readonly target: MemberExpressionNode | IdentifierNode; readonly arguments: Expression[]; + readonly argumentsRange: TextRange; } export interface InvalidStatementNode extends BaseNode { @@ -1452,6 +1488,7 @@ export interface EmptyStatementNode extends BaseNode { export interface ModelExpressionNode extends BaseNode { readonly kind: SyntaxKind.ModelExpression; readonly properties: (ModelPropertyNode | ModelSpreadPropertyNode)[]; + readonly propertiesRange: TextRange; } export interface ArrayExpressionNode extends BaseNode { @@ -1461,6 +1498,7 @@ export interface ArrayExpressionNode extends BaseNode { export interface TupleExpressionNode extends BaseNode { readonly kind: SyntaxKind.TupleExpression; readonly values: readonly Expression[]; + readonly valuesRange: TextRange; } export interface ModelPropertyNode extends BaseNode { @@ -1482,6 +1520,7 @@ export interface ModelSpreadPropertyNode extends BaseNode { export interface ObjectLiteralNode extends BaseNode { readonly kind: SyntaxKind.ObjectLiteral; readonly properties: (ObjectLiteralPropertyNode | ObjectLiteralSpreadPropertyNode)[]; + readonly propertiesRange: TextRange; } export interface ObjectLiteralPropertyNode extends BaseNode { @@ -1500,6 +1539,7 @@ export interface ObjectLiteralSpreadPropertyNode extends BaseNode { export interface ArrayLiteralNode extends BaseNode { readonly kind: SyntaxKind.ArrayLiteral; readonly values: readonly Expression[]; + readonly valuesRange: TextRange; } export type LiteralNode = @@ -1604,6 +1644,7 @@ export interface TypeReferenceNode extends BaseNode { readonly kind: SyntaxKind.TypeReference; readonly target: MemberExpressionNode | IdentifierNode; readonly arguments: readonly TemplateArgumentNode[]; + readonly argumentsRange: TextRange; } export interface TemplateArgumentNode extends BaseNode { @@ -1652,6 +1693,10 @@ export interface DecoratorDeclarationStatementNode extends BaseNode, Declaration * Additional parameters */ readonly parameters: FunctionParameterNode[]; + /** + * range including target and parameters + */ + readonly allParametersRange: TextRange; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; } @@ -1683,6 +1728,7 @@ export interface FunctionDeclarationStatementNode extends BaseNode, DeclarationN readonly modifiers: readonly Modifier[]; readonly modifierFlags: ModifierFlags; readonly parameters: FunctionParameterNode[]; + readonly parametersRange: TextRange; readonly returnType?: Expression; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; } diff --git a/packages/compiler/src/server/completion.ts b/packages/compiler/src/server/completion.ts index 44a1284f70..eb96a96628 100644 --- a/packages/compiler/src/server/completion.ts +++ b/packages/compiler/src/server/completion.ts @@ -11,6 +11,7 @@ import { getDeprecationDetails } from "../core/deprecation.js"; import { CompilerHost, IdentifierNode, + Node, NodeFlags, NodePackage, PositionDetail, @@ -20,6 +21,9 @@ import { SyntaxKind, Type, TypeSpecScriptNode, + compilerAssert, + getFirstAncestor, + positionInRange, } from "../core/index.js"; import { getAnyExtensionFromPath, @@ -41,11 +45,112 @@ export type CompletionContext = { export async function resolveCompletion( context: CompletionContext, - posDetail: PositionDetail | undefined + posDetail: PositionDetail ): Promise { - const node = posDetail?.node; + let node: Node | undefined = posDetail.node; + + if (!node) { + if ( + posDetail.triviaStartPosition === 0 || + !addCompletionByLookingBackward(posDetail, context) + ) { + addKeywordCompletion("root", context.completions); + } + } else { + // look back first to see whether we can get some completion from the previous statement, e.g. `model Foo |` + if (!addCompletionByLookingBackward(posDetail, context)) { + if (posDetail.inTrivia) { + // If we're not immediately after an identifier character, then advance + // the position past any trivia. This is done because a zero-width + // inserted missing identifier that the user is now trying to complete + // starts after the trivia following the cursor. + node = posDetail.getPositionDetailAfterTrivia().node; + } + await AddCompletionNonTrivia(node, context, posDetail); + } else { + if (!posDetail.inTrivia) { + await AddCompletionNonTrivia(node, context, posDetail); + } + } + } + + return context.completions; +} + +function addCompletionByLookingBackward( + posDetail: PositionDetail, + context: CompletionContext +): boolean { + if (posDetail.triviaStartPosition === 0) { + return false; + } + const preDetail = posDetail.getPositionDetailBeforeTrivia(); + if (!preDetail.node) { + return false; + } + + const node = getFirstAncestor( + preDetail.node, + (n) => + n.kind === SyntaxKind.ModelStatement || + n.kind === SyntaxKind.ScalarStatement || + n.kind === SyntaxKind.OperationStatement || + n.kind === SyntaxKind.InterfaceStatement || + n.kind === SyntaxKind.TemplateParameterDeclaration, + true /*includeSelf*/ + ); + + return node !== undefined && addCompletionByLookingBackwardNode(node, posDetail, context); +} + +function addCompletionByLookingBackwardNode( + preNode: Node, + posDetail: PositionDetail, + context: CompletionContext +): boolean { + const map: { [key in SyntaxKind]?: keyof KeywordArea } = { + [SyntaxKind.ModelStatement]: "modelHeader", + [SyntaxKind.ScalarStatement]: "scalarHeader", + [SyntaxKind.OperationStatement]: "operationHeader", + [SyntaxKind.InterfaceStatement]: "interfaceHeader", + }; + switch (preNode?.kind) { + case SyntaxKind.ModelStatement: + case SyntaxKind.ScalarStatement: + case SyntaxKind.OperationStatement: + case SyntaxKind.InterfaceStatement: + const idEndPos = + preNode.templateParameters.length > 0 + ? preNode.templateParametersRange.end + : preNode.id.end; + if (posDetail.triviaStartPosition === idEndPos) { + const key = map[preNode.kind]; + if (!key) { + compilerAssert(false, "KeywordArea missing in keyarea map."); + } + addKeywordCompletion(key, context.completions); + return true; + } + break; + case SyntaxKind.TemplateParameterDeclaration: + if (posDetail.triviaStartPosition === preNode.id.end) { + addKeywordCompletion("templateParameter", context.completions); + return true; + } else if (preNode.parent?.templateParametersRange.end === posDetail.triviaStartPosition) { + return addCompletionByLookingBackwardNode(preNode.parent, posDetail, context); + } + break; + } + return false; +} + +async function AddCompletionNonTrivia( + node: Node | undefined, + context: CompletionContext, + posDetail: PositionDetail, + lookBackward: boolean = true +) { if ( - posDetail === undefined || node === undefined || node.kind === SyntaxKind.InvalidStatement || (node.kind === SyntaxKind.Identifier && @@ -59,7 +164,9 @@ export async function resolveCompletion( addKeywordCompletion("namespace", context.completions); break; case SyntaxKind.ScalarStatement: - addKeywordCompletion("scalar", context.completions); + if (positionInRange(posDetail.position, node.membersRange)) { + addKeywordCompletion("scalarBody", context.completions); + } break; case SyntaxKind.Identifier: addDirectiveCompletion(context, node); @@ -77,16 +184,18 @@ export async function resolveCompletion( break; } } - - return context.completions; } interface KeywordArea { root?: boolean; namespace?: boolean; - model?: boolean; + modelHeader?: boolean; identifier?: boolean; - scalar?: boolean; + scalarHeader?: boolean; + scalarBody?: boolean; + templateParameter?: boolean; + operationHeader?: boolean; + interfaceHeader?: boolean; } const keywords = [ @@ -108,8 +217,11 @@ const keywords = [ ["const", { root: true, namespace: true }], // On model `model Foo ...` - ["extends", { model: true }], - ["is", { model: true }], + [ + "extends", + { modelHeader: true, scalarHeader: true, templateParameter: true, interfaceHeader: true }, + ], + ["is", { modelHeader: true, operationHeader: true }], // On identifier ["true", { identifier: true }], @@ -122,7 +234,7 @@ const keywords = [ ["extern", { root: true, namespace: true }], // Scalars - ["init", { scalar: true }], + ["init", { scalarBody: true }], ] as const; function addKeywordCompletion(area: keyof KeywordArea, completions: CompletionList) { @@ -248,34 +360,37 @@ 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 + !node || + (node.kind !== SyntaxKind.ModelStatement && + node.kind !== SyntaxKind.ModelExpression && + node.kind !== SyntaxKind.ObjectLiteral) ) { return; } - // skip the scenario like `{ ... }|` - if (node.end === posDetail.position) { + + if (posDetail.position === node.propertiesRange.end) { + // skip the scenario like `{ ... }|` return; + } else { + // 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); } - // 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); } /** diff --git a/packages/compiler/src/server/serverlib.ts b/packages/compiler/src/server/serverlib.ts index f6a340b5bf..eadc1878cf 100644 --- a/packages/compiler/src/server/serverlib.ts +++ b/packages/compiler/src/server/serverlib.ts @@ -45,7 +45,7 @@ import { WorkspaceEdit, WorkspaceFoldersChangeEvent, } from "vscode-languageserver/node.js"; -import { CharCode, codePointBefore, isIdentifierContinue } from "../core/charcode.js"; +import { CharCode } from "../core/charcode.js"; import { resolveCodeFix } from "../core/code-fixes.js"; import { compilerAssert, getSourceLocation } from "../core/diagnostics.js"; import { formatTypeSpec } from "../core/formatter.js"; @@ -1077,21 +1077,6 @@ export function getCompletionNodeAtPosition( script: TypeSpecScriptNode, position: number, filter: (node: Node) => boolean = (node: Node) => true -): 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 - // inserted missing identifier that the user is now trying to complete - // starts after the trivia following the cursor. - const cp = codePointBefore(script.file.text, position); - if (!cp || !isIdentifierContinue(cp)) { - const newPosition = skipTrivia(script.file.text, position); - if (newPosition !== position) { - return getNodeAtPositionDetail(script, newPosition, filter); - } - } - return detail; +): PositionDetail { + return getNodeAtPositionDetail(script, position, filter); } diff --git a/packages/compiler/test/server/completion.test.ts b/packages/compiler/test/server/completion.test.ts index 90b78e8f71..dd0b199ed5 100644 --- a/packages/compiler/test/server/completion.test.ts +++ b/packages/compiler/test/server/completion.test.ts @@ -1,4 +1,4 @@ -import { deepStrictEqual, ok, strictEqual } from "assert"; +import { deepStrictEqual, equal, ok, strictEqual } from "assert"; import { describe, it } from "vitest"; import { CompletionItem, @@ -60,6 +60,115 @@ describe("complete statement keywords", () => { }); }); +describe("completes for keywords", () => { + describe.each([ + [`scalar S ┆`, ["extends"]], + [`scalar S ┆ `, ["extends"]], + [`scalar S \n┆\n`, ["extends"]], + [`scalar S ┆;`, ["extends"]], + [`scalar S ┆ ;`, ["extends"]], + [`scalar S /*comment*/ ┆{}`, ["extends"]], + [`scalar S ┆ {}`, ["extends"]], + [`scalar S ┆ \nscalar S2`, ["extends"]], + [`scalar S1;\nscalar S2 ┆ S1`, ["extends"]], + [`scalar S1;\nscalar S2 e┆x S1`, ["extends"]], + [`scalar S1;\nscalar S2 ┆ex S1`, ["extends"]], + [`scalar S ┆\n`, ["extends"]], + [`scalar S┆ \n`, ["extends"]], + [`scalar S ┆ {}`, ["extends"]], + [`scalar S ex┆`, ["extends"]], + [`scalar S ex┆tends`, ["extends"]], + [`scalar S ex ┆ {}`, []], + [`scalar S ex ex┆`, []], + [`scalar S {┆}`, ["init"]], + [`scalar S`, []], + + [`model M ┆`, ["extends", "is"]], + [`model M ┆ `, ["extends", "is"]], + [`model M \n┆\n`, ["extends", "is"]], + [`model M ┆;`, ["extends", "is"]], + [`model M ┆ ;`, ["extends", "is"]], + [`model M ┆{}`, ["extends", "is"]], + [`model M ┆ {}`, ["extends", "is"]], + [`model M ┆ \nscalar S2`, ["extends", "is"]], + [`model M1{}; model M2 ┆ M1`, ["extends", "is"]], + [`model M1{}; model M2 e┆x M1`, ["extends", "is"]], + [`model M1{}; model M2 ┆ex M1`, ["extends", "is"]], + [`model M ┆\n`, ["extends", "is"]], + [`model M┆ \n`, ["extends", "is"]], + [`model M ┆ {}`, ["extends", "is"]], + [`model M ex┆`, ["extends", "is"]], + [`model M ex┆tends`, ["extends", "is"]], + [`model M {┆}`, []], + [`model M {}`, []], + + [`op o ┆`, ["is"]], + [`op o ┆ `, ["is"]], + [`op o \n┆\n`, ["is"]], + [`op o ┆;`, ["is"]], + [`op o ┆ ;`, ["is"]], + [`op o ┆{}`, ["is"]], + [`op o ┆ {}`, ["is"]], + [`op o ┆ \nscalar S2`, ["is"]], + [`op o1{}; op o2 \n//comment\n ┆ M1`, ["is"]], + [`op o1{}; op o2 i┆s M1`, ["is"]], + [`op o1{}; op o2 ┆is M1`, ["is"]], + [`op o ┆\n`, ["is"]], + [`op o┆ \n`, ["is"]], + [`op o ┆ {}`, ["is"]], + [`op o is┆`, ["is"]], + [`op o (┆)`, []], + [`op o {}`, []], + [`interface I {o ┆}`, ["is"]], + [`interface I {o ┆ ()}`, ["is"]], + [`interface I {o (┆)}`, []], + + [`interface I ┆`, ["extends"]], + [`interface I //comment\n ┆ `, ["extends"]], + [`interface I \n┆\n`, ["extends"]], + [`interface I ┆;`, ["extends"]], + [`interface I ┆ ;`, ["extends"]], + [`interface I ┆{}`, ["extends"]], + [`interface I ┆ {}`, ["extends"]], + [`interface I ┆ \nscalar S2`, ["extends"]], + [`interface I1;\ninterface I2 ┆ I1`, ["extends"]], + [`interface I1;\ninterface I2 e┆x I1`, ["extends"]], + [`interface I1;\ninterface I2 ┆ex I1`, ["extends"]], + [`interface I ┆\n`, ["extends"]], + [`interface I┆ \n`, ["extends"]], + [`interface I ┆ {}`, ["extends"]], + [`interface I ex┆`, ["extends"]], + [`interface I ex┆tends`, ["extends"]], + [`interface I ex ┆ {}`, []], + [`interface I ex ex┆`, []], + [`interface I {┆}`, []], + [`interface I`, []], + + [`scalar S`, ["extends"]], + [`scalar S`, ["extends"]], + [`model M`, ["extends"]], + [`model M`, ["extends"]], + [`model M`, ["extends"]], + [`op o`, ["extends"]], + [`op o`, ["extends"]], + [`interface I┆`, []], + [`model M{};alias a = M`, []], + [`model M{};model M2 extends M`, []], + ] as const)("%s", (code, keywords) => { + it("completes extends keyword", async () => { + const completions = await complete(code); + if (keywords.length > 0) { + check( + completions, + keywords.map((w) => ({ label: w, kind: CompletionItemKind.Keyword })) + ); + } else { + equal(completions.items.length, 0, "No completions expected"); + } + }); + }); +}); + describe("imports", () => { describe("library imports", () => { async function testCompleteLibrary(code: string) { diff --git a/packages/compiler/test/server/misc.test.ts b/packages/compiler/test/server/misc.test.ts index 8ff5218b52..176d8f7137 100644 --- a/packages/compiler/test/server/misc.test.ts +++ b/packages/compiler/test/server/misc.test.ts @@ -1,6 +1,6 @@ import { ok, strictEqual } from "assert"; import { describe, it } from "vitest"; -import { Node, SyntaxKind, TypeSpecScriptNode, parse } from "../../src/core/index.js"; +import { PositionDetail, SyntaxKind, TypeSpecScriptNode, parse } from "../../src/core/index.js"; import { getCompletionNodeAtPosition } from "../../src/server/serverlib.js"; import { extractCursor } from "../../src/testing/test-server-host.js"; import { dumpAST } from "../ast-test-utils.js"; @@ -9,57 +9,62 @@ describe("compiler: server: misc", () => { describe("getCompletionNodeAtPosition", () => { async function getNodeAtCursor( sourceWithCursor: string - ): Promise<{ root: TypeSpecScriptNode; node: Node | undefined }> { + ): Promise<{ root: TypeSpecScriptNode; detail: PositionDetail | undefined }> { const { source, pos } = extractCursor(sourceWithCursor); - const root = parse(source); + const root = parse(source, { comments: true, docs: true }); dumpAST(root); - return { node: getCompletionNodeAtPosition(root, pos)?.node, root }; + return { detail: getCompletionNodeAtPosition(root, pos), root }; } it("return identifier for property return type", async () => { - const { node } = await getNodeAtCursor(` + const { detail } = await getNodeAtCursor(` model Foo { prop: stri┆ng } `); + const node = detail?.node; ok(node); strictEqual(node.kind, SyntaxKind.Identifier as const); strictEqual(node.sv, "string"); }); it("return missing identifier node when at the position for model property type", async () => { - const { node } = await getNodeAtCursor(` + const { detail } = await getNodeAtCursor(` model Foo { prop: ┆ } `); + const node = detail?.getPositionDetailAfterTrivia()?.node; ok(node); strictEqual(node.kind, SyntaxKind.Identifier as const); strictEqual(node.sv, "1"); }); it("return string literal when in non completed string", async () => { - const { node } = await getNodeAtCursor(` + const { detail } = await getNodeAtCursor(` import "┆ `); + const node = detail?.node; ok(node); strictEqual(node.kind, SyntaxKind.StringLiteral); }); it("return string literal when in non completed multi line string", async () => { - const { node } = await getNodeAtCursor(` + const { detail } = await getNodeAtCursor(` model Foo { prop: """┆ } `); + const node = detail?.node; ok(node); strictEqual(node.kind, SyntaxKind.StringLiteral); }); it("return missing identifier between dot and close paren", async () => { - const { node } = await getNodeAtCursor(` + const { detail } = await getNodeAtCursor(` @myDecN.┆) `); + const node = detail?.node; ok(node); strictEqual(node.kind, SyntaxKind.Identifier as const); strictEqual(node.sv, "1"); @@ -67,11 +72,12 @@ describe("compiler: server: misc", () => { describe("resolve real node when no potential identifier", () => { it("return namespace when in namespace body", async () => { - const { node } = await getNodeAtCursor(` + const { detail } = await getNodeAtCursor(` namespace Foo { ┆ } `); + const node = detail?.node; ok(node); strictEqual(node.kind, SyntaxKind.NamespaceStatement as const); strictEqual(node.id.sv, "Foo"); From 2f9b75a9b8d80bc6cf30291b3528aba5d8ae4ca6 Mon Sep 17 00:00:00 2001 From: RodgeFu Date: Fri, 24 May 2024 16:05:16 +0800 Subject: [PATCH 2/4] add change log --- ...letion-for-extends-is-2024-4-24-11-15-1.md | 27 +++++++++++++++++++ packages/compiler/src/server/completion.ts | 10 ++++--- .../compiler/test/server/completion.test.ts | 6 ++++- 3 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 .chronus/changes/ide-support-completion-for-extends-is-2024-4-24-11-15-1.md diff --git a/.chronus/changes/ide-support-completion-for-extends-is-2024-4-24-11-15-1.md b/.chronus/changes/ide-support-completion-for-extends-is-2024-4-24-11-15-1.md new file mode 100644 index 0000000000..e3c10d8967 --- /dev/null +++ b/.chronus/changes/ide-support-completion-for-extends-is-2024-4-24-11-15-1.md @@ -0,0 +1,27 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Support completion for keyword 'extends' and 'is' + + Example + ```tsp + model Dog ┆ {} + | [extends] + | [is] + + scalar Addresss ┆ + | [extends] + + op jump ┆ + | [is] + + interface ResourceA ┆ {} + | [extends] + + model Cat {} + | [extends] + ``` + diff --git a/packages/compiler/src/server/completion.ts b/packages/compiler/src/server/completion.ts index eb96a96628..a936bd3d23 100644 --- a/packages/compiler/src/server/completion.ts +++ b/packages/compiler/src/server/completion.ts @@ -108,6 +108,10 @@ function addCompletionByLookingBackwardNode( posDetail: PositionDetail, context: CompletionContext ): boolean { + const getIdentifierEndPos = (n: IdentifierNode) => { + // n.pos === n.end, it means it's a missing identifier, just return -1; + return n.pos === n.end ? -1 : n.end; + }; const map: { [key in SyntaxKind]?: keyof KeywordArea } = { [SyntaxKind.ModelStatement]: "modelHeader", [SyntaxKind.ScalarStatement]: "scalarHeader", @@ -120,9 +124,9 @@ function addCompletionByLookingBackwardNode( case SyntaxKind.OperationStatement: case SyntaxKind.InterfaceStatement: const idEndPos = - preNode.templateParameters.length > 0 + preNode.templateParametersRange.end >= 0 ? preNode.templateParametersRange.end - : preNode.id.end; + : getIdentifierEndPos(preNode.id); if (posDetail.triviaStartPosition === idEndPos) { const key = map[preNode.kind]; if (!key) { @@ -133,7 +137,7 @@ function addCompletionByLookingBackwardNode( } break; case SyntaxKind.TemplateParameterDeclaration: - if (posDetail.triviaStartPosition === preNode.id.end) { + if (posDetail.triviaStartPosition === getIdentifierEndPos(preNode.id)) { addKeywordCompletion("templateParameter", context.completions); return true; } else if (preNode.parent?.templateParametersRange.end === posDetail.triviaStartPosition) { diff --git a/packages/compiler/test/server/completion.test.ts b/packages/compiler/test/server/completion.test.ts index dd0b199ed5..6e755c8ea8 100644 --- a/packages/compiler/test/server/completion.test.ts +++ b/packages/compiler/test/server/completion.test.ts @@ -98,7 +98,7 @@ describe("completes for keywords", () => { [`model M┆ \n`, ["extends", "is"]], [`model M ┆ {}`, ["extends", "is"]], [`model M ex┆`, ["extends", "is"]], - [`model M ex┆tends`, ["extends", "is"]], + [`model M i┆s`, ["extends", "is"]], [`model M {┆}`, []], [`model M {}`, []], @@ -109,6 +109,8 @@ describe("completes for keywords", () => { [`op o ┆ ;`, ["is"]], [`op o ┆{}`, ["is"]], [`op o ┆ {}`, ["is"]], + [`op o ┆ ()`, ["is"]], + [`op o ┆()`, ["is"]], [`op o ┆ \nscalar S2`, ["is"]], [`op o1{}; op o2 \n//comment\n ┆ M1`, ["is"]], [`op o1{}; op o2 i┆s M1`, ["is"]], @@ -152,6 +154,8 @@ describe("completes for keywords", () => { [`op o`, ["extends"]], [`op o`, ["extends"]], [`interface I┆`, []], + [`interface I<┆, T, Q>`, []], + [`interface I`, ["extends"]], [`model M{};alias a = M`, []], [`model M{};model M2 extends M`, []], ] as const)("%s", (code, keywords) => { From 8a83e7de0745562826ef79e57748bb7818e27d60 Mon Sep 17 00:00:00 2001 From: RodgeFu Date: Thu, 30 May 2024 16:36:25 +0800 Subject: [PATCH 3/4] update per comment --- packages/compiler/src/core/parser.ts | 64 +++++++--------------- packages/compiler/src/core/types.ts | 27 ++------- packages/compiler/src/server/completion.ts | 4 +- 3 files changed, 29 insertions(+), 66 deletions(-) diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index cef9973529..9e26d5056f 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -726,7 +726,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa nextToken(); } - const { items: operations, range: operationsRange } = parseList( + const { items: operations, range: bodyRange } = parseList( ListKind.InterfaceMembers, (pos, decorators) => parseOperationStatement(pos, decorators, true) ); @@ -737,7 +737,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa templateParameters, templateParametersRange, operations, - operationsRange, + bodyRange, extends: extendList.items, decorators, ...finishNode(pos), @@ -902,14 +902,14 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseOperationParameters(): ModelExpressionNode { const pos = tokenPos(); - const { items: properties, range: propertiesRange } = parseList( + const { items: properties, range: bodyRange } = parseList( ListKind.OperationParameters, parseModelPropertyOrSpread ); const parameters: ModelExpressionNode = { kind: SyntaxKind.ModelExpression, properties, - propertiesRange, + bodyRange, ...finishNode(pos), }; return parameters; @@ -952,7 +952,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa templateParametersRange, decorators, properties: propDetail.items, - propertiesRange: propDetail.range, + bodyRange: propDetail.range, ...finishNode(pos), }; } @@ -1125,7 +1125,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa parseTemplateParameterList(); const optionalExtends = parseOptionalScalarExtends(); - const { items: members, range: membersRange } = parseScalarMembers(); + const { items: members, range: bodyRange } = parseScalarMembers(); return { kind: SyntaxKind.ScalarStatement, @@ -1134,7 +1134,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa templateParametersRange, extends: optionalExtends, members, - membersRange, + bodyRange, decorators, ...finishNode(pos), }; @@ -1164,12 +1164,11 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa parseExpected(Token.InitKeyword); const id = parseIdentifier(); - const { items: parameters, range: parametersRange } = parseFunctionParameters(); + const { items: parameters } = parseFunctionParameters(); return { kind: SyntaxKind.ScalarConstructor, id, parameters, - parametersRange, ...finishNode(pos), }; } @@ -1180,7 +1179,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ): EnumStatementNode { parseExpected(Token.EnumKeyword); const id = parseIdentifier(); - const { items: members, range: membersRange } = parseList( + const { items: members, range: bodyRange } = parseList( ListKind.EnumMembers, parseEnumMemberOrSpread ); @@ -1189,7 +1188,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa id, decorators, members, - membersRange, + bodyRange, ...finishNode(pos), }; } @@ -1427,15 +1426,11 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const pos = tokenPos(); const target = parseIdentifierOrMemberExpression(message); if (token() === Token.OpenParen) { - const { items: args, range: argumentsRange } = parseList( - ListKind.FunctionArguments, - parseExpression - ); + const { items: args } = parseList(ListKind.FunctionArguments, parseExpression); return { kind: SyntaxKind.CallExpression, target, arguments: args, - argumentsRange, ...finishNode(pos), }; } @@ -1447,16 +1442,12 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa target: IdentifierNode | MemberExpressionNode, pos: number ): TypeReferenceNode { - const { items: args, range: argumentsRange } = parseOptionalList( - ListKind.TemplateArguments, - parseTemplateArgument - ); + const { items: args } = parseOptionalList(ListKind.TemplateArguments, parseTemplateArgument); return { kind: SyntaxKind.TypeReference, target, arguments: args, - argumentsRange, ...finishNode(pos), }; } @@ -1525,11 +1516,9 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa kind: SyntaxKind.TypeReference, target: createMissingIdentifier(), arguments: emptyList.items, - argumentsRange: emptyList.range, ...finishNode(pos), }, arguments: args, - allArgumentsRange: argRange, ...finishNode(pos), }; } @@ -1541,7 +1530,6 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa kind: SyntaxKind.TypeReference, target: createMissingIdentifier(), arguments: emptyList.items, - argumentsRange: emptyList.range, ...finishNode(pos), }; } @@ -1553,7 +1541,6 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa target, targetType: targetEntity, arguments: decoratorArgs, - allArgumentsRange: argRange, ...finishNode(pos), }; } @@ -1580,14 +1567,10 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa // `@` applied to `model Foo`, and not as `@model` // applied to invalid statement `Foo`. const target = parseIdentifierOrMemberExpression(undefined, false); - const { items: args, range: argsRange } = parseOptionalList( - ListKind.DecoratorArguments, - parseExpression - ); + const { items: args } = parseOptionalList(ListKind.DecoratorArguments, parseExpression); return { kind: SyntaxKind.DecoratorExpression, arguments: args, - argumentsRange: argsRange, target, ...finishNode(pos), }; @@ -1784,50 +1767,48 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseTupleExpression(): TupleExpressionNode { const pos = tokenPos(); - const { items: values, range: valuesRange } = parseList(ListKind.Tuple, parseExpression); + const { items: values } = parseList(ListKind.Tuple, parseExpression); return { kind: SyntaxKind.TupleExpression, values, - valuesRange, ...finishNode(pos), }; } function parseModelExpression(): ModelExpressionNode { const pos = tokenPos(); - const { items: properties, range: propertiesRange } = parseList( + const { items: properties, range: bodyRange } = parseList( ListKind.ModelProperties, parseModelPropertyOrSpread ); return { kind: SyntaxKind.ModelExpression, properties, - propertiesRange, + bodyRange, ...finishNode(pos), }; } function parseObjectLiteral(): ObjectLiteralNode { const pos = tokenPos(); - const { items: properties, range: propertiesRange } = parseList( + const { items: properties, range: bodyRange } = parseList( ListKind.ObjectLiteralProperties, parseObjectLiteralPropertyOrSpread ); return { kind: SyntaxKind.ObjectLiteral, properties, - propertiesRange, + bodyRange, ...finishNode(pos), }; } function parseArrayLiteral(): ArrayLiteralNode { const pos = tokenPos(); - const { items: values, range: valuesRange } = parseList(ListKind.ArrayLiteral, parseExpression); + const { items: values } = parseList(ListKind.ArrayLiteral, parseExpression); return { kind: SyntaxKind.ArrayLiteral, values, - valuesRange, ...finishNode(pos), }; } @@ -2071,7 +2052,6 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa id, target, parameters, - allParametersRange: allParamListDetail.range, ...finishNode(pos), }; } @@ -2083,7 +2063,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const modifierFlags = modifiersToFlags(modifiers); parseExpected(Token.FnKeyword); const id = parseIdentifier(); - const { items: parameters, range: parametersRange } = parseFunctionParameters(); + const { items: parameters } = parseFunctionParameters(); let returnType; if (parseOptional(Token.Colon)) { returnType = parseExpression(); @@ -2095,7 +2075,6 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa modifierFlags, id, parameters, - parametersRange, returnType, ...finishNode(pos), }; @@ -3138,13 +3117,12 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function createMissingTypeReference(): TypeReferenceNode { const pos = tokenPos(); - const { items: args, range: argsRange } = createEmptyList(); + const { items: args } = createEmptyList(); return { kind: SyntaxKind.TypeReference, target: createMissingIdentifier(), arguments: args, - argumentsRange: argsRange, ...finishNode(pos), }; } diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index dc3628f02d..6e3220a16f 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -1268,7 +1268,6 @@ export interface DecoratorExpressionNode extends BaseNode { readonly kind: SyntaxKind.DecoratorExpression; readonly target: IdentifierNode | MemberExpressionNode; readonly arguments: readonly Expression[]; - readonly argumentsRange: TextRange; } export interface AugmentDecoratorStatementNode extends BaseNode { @@ -1276,10 +1275,6 @@ export interface AugmentDecoratorStatementNode extends BaseNode { readonly target: IdentifierNode | MemberExpressionNode; readonly targetType: TypeReferenceNode; readonly arguments: readonly Expression[]; - /** - * range including targetType and arguments - */ - readonly allArgumentsRange: TextRange; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; } @@ -1388,7 +1383,7 @@ export interface OperationStatementNode extends BaseNode, DeclarationNode, Templ export interface ModelStatementNode extends BaseNode, DeclarationNode, TemplateDeclarationNode { readonly kind: SyntaxKind.ModelStatement; readonly properties: readonly (ModelPropertyNode | ModelSpreadPropertyNode)[]; - readonly propertiesRange: TextRange; + readonly bodyRange: TextRange; readonly extends?: Expression; readonly is?: Expression; readonly decorators: readonly DecoratorExpressionNode[]; @@ -1400,7 +1395,7 @@ export interface ScalarStatementNode extends BaseNode, DeclarationNode, Template readonly extends?: TypeReferenceNode; readonly decorators: readonly DecoratorExpressionNode[]; readonly members: readonly ScalarConstructorNode[]; - readonly membersRange: TextRange; + readonly bodyRange: TextRange; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; } @@ -1408,14 +1403,13 @@ export interface ScalarConstructorNode extends BaseNode { readonly kind: SyntaxKind.ScalarConstructor; readonly id: IdentifierNode; readonly parameters: FunctionParameterNode[]; - readonly parametersRange: TextRange; readonly parent?: ScalarStatementNode; } export interface InterfaceStatementNode extends BaseNode, DeclarationNode, TemplateDeclarationNode { readonly kind: SyntaxKind.InterfaceStatement; readonly operations: readonly OperationStatementNode[]; - readonly operationsRange: TextRange; + readonly bodyRange: TextRange; readonly extends: readonly TypeReferenceNode[]; readonly decorators: readonly DecoratorExpressionNode[]; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; @@ -1439,7 +1433,7 @@ export interface UnionVariantNode extends BaseNode { export interface EnumStatementNode extends BaseNode, DeclarationNode { readonly kind: SyntaxKind.EnumStatement; readonly members: readonly (EnumMemberNode | EnumSpreadMemberNode)[]; - readonly membersRange: TextRange; + readonly bodyRange: TextRange; readonly decorators: readonly DecoratorExpressionNode[]; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; } @@ -1473,7 +1467,6 @@ export interface CallExpressionNode extends BaseNode { readonly kind: SyntaxKind.CallExpression; readonly target: MemberExpressionNode | IdentifierNode; readonly arguments: Expression[]; - readonly argumentsRange: TextRange; } export interface InvalidStatementNode extends BaseNode { @@ -1488,7 +1481,7 @@ export interface EmptyStatementNode extends BaseNode { export interface ModelExpressionNode extends BaseNode { readonly kind: SyntaxKind.ModelExpression; readonly properties: (ModelPropertyNode | ModelSpreadPropertyNode)[]; - readonly propertiesRange: TextRange; + readonly bodyRange: TextRange; } export interface ArrayExpressionNode extends BaseNode { @@ -1498,7 +1491,6 @@ export interface ArrayExpressionNode extends BaseNode { export interface TupleExpressionNode extends BaseNode { readonly kind: SyntaxKind.TupleExpression; readonly values: readonly Expression[]; - readonly valuesRange: TextRange; } export interface ModelPropertyNode extends BaseNode { @@ -1520,7 +1512,7 @@ export interface ModelSpreadPropertyNode extends BaseNode { export interface ObjectLiteralNode extends BaseNode { readonly kind: SyntaxKind.ObjectLiteral; readonly properties: (ObjectLiteralPropertyNode | ObjectLiteralSpreadPropertyNode)[]; - readonly propertiesRange: TextRange; + readonly bodyRange: TextRange; } export interface ObjectLiteralPropertyNode extends BaseNode { @@ -1539,7 +1531,6 @@ export interface ObjectLiteralSpreadPropertyNode extends BaseNode { export interface ArrayLiteralNode extends BaseNode { readonly kind: SyntaxKind.ArrayLiteral; readonly values: readonly Expression[]; - readonly valuesRange: TextRange; } export type LiteralNode = @@ -1644,7 +1635,6 @@ export interface TypeReferenceNode extends BaseNode { readonly kind: SyntaxKind.TypeReference; readonly target: MemberExpressionNode | IdentifierNode; readonly arguments: readonly TemplateArgumentNode[]; - readonly argumentsRange: TextRange; } export interface TemplateArgumentNode extends BaseNode { @@ -1693,10 +1683,6 @@ export interface DecoratorDeclarationStatementNode extends BaseNode, Declaration * Additional parameters */ readonly parameters: FunctionParameterNode[]; - /** - * range including target and parameters - */ - readonly allParametersRange: TextRange; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; } @@ -1728,7 +1714,6 @@ export interface FunctionDeclarationStatementNode extends BaseNode, DeclarationN readonly modifiers: readonly Modifier[]; readonly modifierFlags: ModifierFlags; readonly parameters: FunctionParameterNode[]; - readonly parametersRange: TextRange; readonly returnType?: Expression; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; } diff --git a/packages/compiler/src/server/completion.ts b/packages/compiler/src/server/completion.ts index a936bd3d23..f8474066f3 100644 --- a/packages/compiler/src/server/completion.ts +++ b/packages/compiler/src/server/completion.ts @@ -168,7 +168,7 @@ async function AddCompletionNonTrivia( addKeywordCompletion("namespace", context.completions); break; case SyntaxKind.ScalarStatement: - if (positionInRange(posDetail.position, node.membersRange)) { + if (positionInRange(posDetail.position, node.bodyRange)) { addKeywordCompletion("scalarBody", context.completions); } break; @@ -372,7 +372,7 @@ function addModelCompletion(context: CompletionContext, posDetail: PositionDetai return; } - if (posDetail.position === node.propertiesRange.end) { + if (posDetail.position === node.bodyRange.end) { // skip the scenario like `{ ... }|` return; } else { From 30acf47b032575dfa814572fd09faec82987432b Mon Sep 17 00:00:00 2001 From: RodgeFu Date: Fri, 31 May 2024 13:37:06 +0800 Subject: [PATCH 4/4] fix lint --- packages/compiler/src/core/parser.ts | 11 ++--------- packages/compiler/src/core/types.ts | 1 - 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 9e26d5056f..352ab20c25 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -1179,16 +1179,12 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ): EnumStatementNode { parseExpected(Token.EnumKeyword); const id = parseIdentifier(); - const { items: members, range: bodyRange } = parseList( - ListKind.EnumMembers, - parseEnumMemberOrSpread - ); + const { items: members } = parseList(ListKind.EnumMembers, parseEnumMemberOrSpread); return { kind: SyntaxKind.EnumStatement, id, decorators, members, - bodyRange, ...finishNode(pos), }; } @@ -1502,10 +1498,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa // `@` applied to `model Foo`, and not as `@model` // applied to invalid statement `Foo`. const target = parseIdentifierOrMemberExpression(undefined, false); - const { items: args, range: argRange } = parseOptionalList( - ListKind.DecoratorArguments, - parseExpression - ); + const { items: args } = parseOptionalList(ListKind.DecoratorArguments, parseExpression); if (args.length === 0) { error({ code: "augment-decorator-target" }); const emptyList = createEmptyList(); diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 6e3220a16f..aa52a21d62 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -1433,7 +1433,6 @@ export interface UnionVariantNode extends BaseNode { export interface EnumStatementNode extends BaseNode, DeclarationNode { readonly kind: SyntaxKind.EnumStatement; readonly members: readonly (EnumMemberNode | EnumSpreadMemberNode)[]; - readonly bodyRange: TextRange; readonly decorators: readonly DecoratorExpressionNode[]; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; }