From 0d36d0e39fc9b9833a61b51d66be1f762fa8f454 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 8 Jun 2017 17:21:36 -0700 Subject: [PATCH] Support completions for qualified names in JSDoc (#16380) * Support completions for qualified names in JSDoc * Fix typo --- src/compiler/declarationEmitter.ts | 2 +- src/compiler/parser.ts | 4 +- src/compiler/utilities.ts | 15 ++++ src/services/classifier.ts | 6 +- src/services/completions.ts | 4 +- src/services/services.ts | 80 +++++++++---------- src/services/utilities.ts | 6 +- .../completionInJsDocQualifiedNames.ts | 15 ++++ 8 files changed, 79 insertions(+), 53 deletions(-) create mode 100644 tests/cases/fourslash/completionInJsDocQualifiedNames.ts diff --git a/src/compiler/declarationEmitter.ts b/src/compiler/declarationEmitter.ts index 987157843b6..007620ad510 100644 --- a/src/compiler/declarationEmitter.ts +++ b/src/compiler/declarationEmitter.ts @@ -596,7 +596,7 @@ namespace ts { currentIdentifiers = node.identifiers; isCurrentFileExternalModule = isExternalModule(node); enclosingDeclaration = node; - emitDetachedComments(currentText, currentLineMap, writer, writeCommentRange, node, newLine, /*removeComents*/ true); + emitDetachedComments(currentText, currentLineMap, writer, writeCommentRange, node, newLine, /*removeComments*/ true); emitLines(node.statements); } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 526329b2056..76762ddac00 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -15,7 +15,7 @@ namespace ts { else if (kind === SyntaxKind.Identifier) { return new (IdentifierConstructor || (IdentifierConstructor = objectAllocator.getIdentifierConstructor()))(kind, pos, end); } - else if (kind < SyntaxKind.FirstNode) { + else if (!isNodeKind(kind)) { return new (TokenConstructor || (TokenConstructor = objectAllocator.getTokenConstructor()))(kind, pos, end); } else { @@ -1103,7 +1103,7 @@ namespace ts { pos = scanner.getStartPos(); } - return kind >= SyntaxKind.FirstNode ? new NodeConstructor(kind, pos, pos) : + return isNodeKind(kind) ? new NodeConstructor(kind, pos, pos) : kind === SyntaxKind.Identifier ? new IdentifierConstructor(kind, pos, pos) : new TokenConstructor(kind, pos, pos); } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 77f1e43c9cb..3f4c78c7376 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4670,6 +4670,16 @@ namespace ts { // All node tests in the following list should *not* reference parent pointers so that // they may be used with transformations. namespace ts { + /* @internal */ + export function isNode(node: Node) { + return isNodeKind(node.kind); + } + + /* @internal */ + export function isNodeKind(kind: SyntaxKind) { + return kind >= SyntaxKind.FirstNode; + } + /** * True if node is of some token syntax kind. * For example, this is true for an IfKeyword but not for an IfStatement. @@ -5308,6 +5318,11 @@ namespace ts { return node.kind >= SyntaxKind.FirstJSDocNode && node.kind <= SyntaxKind.LastJSDocNode; } + /** True if node is of a kind that may contain comment text. */ + export function isJSDocCommentContainingNode(node: Node): boolean { + return node.kind === SyntaxKind.JSDocComment || isJSDocTag(node); + } + // TODO: determine what this does before making it public. /* @internal */ export function isJSDocTag(node: Node): boolean { diff --git a/src/services/classifier.ts b/src/services/classifier.ts index ca8cf52a09b..ff33059630d 100644 --- a/src/services/classifier.ts +++ b/src/services/classifier.ts @@ -724,8 +724,8 @@ namespace ts { pushCommentRange(pos, tag.pos - pos); } - pushClassification(tag.atToken.pos, tag.atToken.end - tag.atToken.pos, ClassificationType.punctuation); - pushClassification(tag.tagName.pos, tag.tagName.end - tag.tagName.pos, ClassificationType.docCommentTagName); + pushClassification(tag.atToken.pos, tag.atToken.end - tag.atToken.pos, ClassificationType.punctuation); // "@" + pushClassification(tag.tagName.pos, tag.tagName.end - tag.tagName.pos, ClassificationType.docCommentTagName); // e.g. "param" pos = tag.tagName.end; @@ -814,7 +814,7 @@ namespace ts { * False will mean that node is not classified and traverse routine should recurse into node contents. */ function tryClassifyNode(node: Node): boolean { - if (isJSDocNode(node)) { + if (isJSDoc(node)) { return true; } diff --git a/src/services/completions.ts b/src/services/completions.ts index 99560ffb4d8..4a6b6170b78 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -445,7 +445,7 @@ namespace ts.Completions { } start = timestamp(); - const previousToken = findPrecedingToken(position, sourceFile); + const previousToken = findPrecedingToken(position, sourceFile, /*startNode*/ undefined, /*includeJsDoc*/ true); log("getCompletionData: Get previous token 1: " + (timestamp() - start)); // The decision to provide completion depends on the contextToken, which is determined through the previousToken. @@ -456,7 +456,7 @@ namespace ts.Completions { // Skip this partial identifier and adjust the contextToken to the token that precedes it. if (contextToken && position <= contextToken.end && isWord(contextToken.kind)) { const start = timestamp(); - contextToken = findPrecedingToken(contextToken.getFullStart(), sourceFile); + contextToken = findPrecedingToken(contextToken.getFullStart(), sourceFile, /*startNode*/ undefined, /*includeJsDoc*/ true); log("getCompletionData: Get previous token 2: " + (timestamp() - start)); } diff --git a/src/services/services.ts b/src/services/services.ts index 22fa67dd1c5..2f835d7de64 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -37,7 +37,7 @@ namespace ts { let ruleProvider: formatting.RulesProvider; function createNode(kind: TKind, pos: number, end: number, parent?: Node): NodeObject | TokenObject | IdentifierObject { - const node = kind >= SyntaxKind.FirstNode ? new NodeObject(kind, pos, end) : + const node = isNodeKind(kind) ? new NodeObject(kind, pos, end) : kind === SyntaxKind.Identifier ? new IdentifierObject(SyntaxKind.Identifier, pos, end) : new TokenObject(kind, pos, end); node.parent = parent; @@ -103,10 +103,10 @@ namespace ts { return sourceFile.text.substring(this.getStart(sourceFile), this.getEnd()); } - private addSyntheticNodes(nodes: Node[], pos: number, end: number, useJSDocScanner?: boolean): number { + private addSyntheticNodes(nodes: Node[], pos: number, end: number): number { scanner.setTextPos(pos); while (pos < end) { - const token = useJSDocScanner ? scanner.scanJSDocToken() : scanner.scan(); + const token = scanner.scan(); Debug.assert(token !== SyntaxKind.EndOfFileToken); // Else it would infinitely loop const textPos = scanner.getTextPos(); if (textPos <= end) { @@ -136,54 +136,50 @@ namespace ts { } private createChildren(sourceFile?: SourceFileLike) { - if (this.kind === SyntaxKind.JSDocComment || isJSDocTag(this)) { + if (!isNodeKind(this.kind)) { + this._children = emptyArray; + return; + } + + if (isJSDocCommentContainingNode(this)) { /** Don't add trivia for "tokens" since this is in a comment. */ const children: Node[] = []; this.forEachChild(child => { children.push(child); }); this._children = children; + return; } - else if (this.kind >= SyntaxKind.FirstNode) { - const children: Node[] = []; - scanner.setText((sourceFile || this.getSourceFile()).text); - let pos = this.pos; - const useJSDocScanner = isJSDocNode(this); - const processNode = (node: Node) => { - const isJSDocTagNode = isJSDocNode(node); - if (!isJSDocTagNode && pos < node.pos) { - pos = this.addSyntheticNodes(children, pos, node.pos, useJSDocScanner); - } - children.push(node); - if (!isJSDocTagNode) { - pos = node.end; - } - }; - const processNodes = (nodes: NodeArray) => { - if (pos < nodes.pos) { - pos = this.addSyntheticNodes(children, pos, nodes.pos, useJSDocScanner); - } - children.push(this.createSyntaxList(nodes)); - pos = nodes.end; - }; - // jsDocComments need to be the first children - if (this.jsDoc) { - for (const jsDocComment of this.jsDoc) { - processNode(jsDocComment); - } + + const children: Node[] = []; + scanner.setText((sourceFile || this.getSourceFile()).text); + let pos = this.pos; + const processNode = (node: Node) => { + pos = this.addSyntheticNodes(children, pos, node.pos); + children.push(node); + pos = node.end; + }; + const processNodes = (nodes: NodeArray) => { + if (pos < nodes.pos) { + pos = this.addSyntheticNodes(children, pos, nodes.pos); } - // For syntactic classifications, all trivia are classcified together, including jsdoc comments. - // For that to work, the jsdoc comments should still be the leading trivia of the first child. - // Restoring the scanner position ensures that. - pos = this.pos; - forEachChild(this, processNode, processNodes); - if (pos < this.end) { - this.addSyntheticNodes(children, pos, this.end); + children.push(this.createSyntaxList(nodes)); + pos = nodes.end; + }; + // jsDocComments need to be the first children + if (this.jsDoc) { + for (const jsDocComment of this.jsDoc) { + processNode(jsDocComment); } - scanner.setText(undefined); - this._children = children; } - else { - this._children = emptyArray; + // For syntactic classifications, all trivia are classcified together, including jsdoc comments. + // For that to work, the jsdoc comments should still be the leading trivia of the first child. + // Restoring the scanner position ensures that. + pos = this.pos; + forEachChild(this, processNode, processNodes); + if (pos < this.end) { + this.addSyntheticNodes(children, pos, this.end); } + scanner.setText(undefined); + this._children = children; } public getChildCount(sourceFile?: SourceFile): number { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index ab958099c11..f9ca077872a 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -710,7 +710,7 @@ namespace ts { } } - export function findPrecedingToken(position: number, sourceFile: SourceFile, startNode?: Node): Node { + export function findPrecedingToken(position: number, sourceFile: SourceFile, startNode?: Node, includeJsDoc?: boolean): Node { return find(startNode || sourceFile); function findRightmostToken(n: Node): Node { @@ -741,7 +741,7 @@ namespace ts { // NOTE: JsxText is a weird kind of node that can contain only whitespaces (since they are not counted as trivia). // if this is the case - then we should assume that token in question is located in previous child. if (position < child.end && (nodeHasTokens(child) || child.kind === SyntaxKind.JsxText)) { - const start = child.getStart(sourceFile); + const start = (includeJsDoc && child.jsDoc ? child.jsDoc[0] : child).getStart(sourceFile); const lookInPreviousChild = (start >= position) || // cursor in the leading trivia (child.kind === SyntaxKind.JsxText && start === child.end); // whitespace only JsxText @@ -758,7 +758,7 @@ namespace ts { } } - Debug.assert(startNode !== undefined || n.kind === SyntaxKind.SourceFile); + Debug.assert(startNode !== undefined || n.kind === SyntaxKind.SourceFile || isJSDocCommentContainingNode(n)); // Here we know that none of child token nodes embrace the position, // the only known case is when position is at the end of the file. diff --git a/tests/cases/fourslash/completionInJsDocQualifiedNames.ts b/tests/cases/fourslash/completionInJsDocQualifiedNames.ts new file mode 100644 index 00000000000..507f6b49fe3 --- /dev/null +++ b/tests/cases/fourslash/completionInJsDocQualifiedNames.ts @@ -0,0 +1,15 @@ +/// + +// @allowJs: true + +// @Filename: /node_modules/foo/index.d.ts +/////** tee */ +////export type T = number; + +// @Filename: /a.js +////import * as Foo from "foo"; +/////** @type {Foo./**/} */ +////const x = 0; + +goTo.marker(); +verify.completionListContains("T", "type T = number", "tee ", "type");