Skip to content

Commit 0d36d0e

Browse files
author
Andy
authored
Support completions for qualified names in JSDoc (#16380)
* Support completions for qualified names in JSDoc * Fix typo
1 parent 1a1d5ea commit 0d36d0e

File tree

8 files changed

+79
-53
lines changed

8 files changed

+79
-53
lines changed

src/compiler/declarationEmitter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,7 @@ namespace ts {
596596
currentIdentifiers = node.identifiers;
597597
isCurrentFileExternalModule = isExternalModule(node);
598598
enclosingDeclaration = node;
599-
emitDetachedComments(currentText, currentLineMap, writer, writeCommentRange, node, newLine, /*removeComents*/ true);
599+
emitDetachedComments(currentText, currentLineMap, writer, writeCommentRange, node, newLine, /*removeComments*/ true);
600600
emitLines(node.statements);
601601
}
602602

src/compiler/parser.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace ts {
1515
else if (kind === SyntaxKind.Identifier) {
1616
return new (IdentifierConstructor || (IdentifierConstructor = objectAllocator.getIdentifierConstructor()))(kind, pos, end);
1717
}
18-
else if (kind < SyntaxKind.FirstNode) {
18+
else if (!isNodeKind(kind)) {
1919
return new (TokenConstructor || (TokenConstructor = objectAllocator.getTokenConstructor()))(kind, pos, end);
2020
}
2121
else {
@@ -1103,7 +1103,7 @@ namespace ts {
11031103
pos = scanner.getStartPos();
11041104
}
11051105

1106-
return kind >= SyntaxKind.FirstNode ? new NodeConstructor(kind, pos, pos) :
1106+
return isNodeKind(kind) ? new NodeConstructor(kind, pos, pos) :
11071107
kind === SyntaxKind.Identifier ? new IdentifierConstructor(kind, pos, pos) :
11081108
new TokenConstructor(kind, pos, pos);
11091109
}

src/compiler/utilities.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4670,6 +4670,16 @@ namespace ts {
46704670
// All node tests in the following list should *not* reference parent pointers so that
46714671
// they may be used with transformations.
46724672
namespace ts {
4673+
/* @internal */
4674+
export function isNode(node: Node) {
4675+
return isNodeKind(node.kind);
4676+
}
4677+
4678+
/* @internal */
4679+
export function isNodeKind(kind: SyntaxKind) {
4680+
return kind >= SyntaxKind.FirstNode;
4681+
}
4682+
46734683
/**
46744684
* True if node is of some token syntax kind.
46754685
* For example, this is true for an IfKeyword but not for an IfStatement.
@@ -5308,6 +5318,11 @@ namespace ts {
53085318
return node.kind >= SyntaxKind.FirstJSDocNode && node.kind <= SyntaxKind.LastJSDocNode;
53095319
}
53105320

5321+
/** True if node is of a kind that may contain comment text. */
5322+
export function isJSDocCommentContainingNode(node: Node): boolean {
5323+
return node.kind === SyntaxKind.JSDocComment || isJSDocTag(node);
5324+
}
5325+
53115326
// TODO: determine what this does before making it public.
53125327
/* @internal */
53135328
export function isJSDocTag(node: Node): boolean {

src/services/classifier.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -724,8 +724,8 @@ namespace ts {
724724
pushCommentRange(pos, tag.pos - pos);
725725
}
726726

727-
pushClassification(tag.atToken.pos, tag.atToken.end - tag.atToken.pos, ClassificationType.punctuation);
728-
pushClassification(tag.tagName.pos, tag.tagName.end - tag.tagName.pos, ClassificationType.docCommentTagName);
727+
pushClassification(tag.atToken.pos, tag.atToken.end - tag.atToken.pos, ClassificationType.punctuation); // "@"
728+
pushClassification(tag.tagName.pos, tag.tagName.end - tag.tagName.pos, ClassificationType.docCommentTagName); // e.g. "param"
729729

730730
pos = tag.tagName.end;
731731

@@ -814,7 +814,7 @@ namespace ts {
814814
* False will mean that node is not classified and traverse routine should recurse into node contents.
815815
*/
816816
function tryClassifyNode(node: Node): boolean {
817-
if (isJSDocNode(node)) {
817+
if (isJSDoc(node)) {
818818
return true;
819819
}
820820

src/services/completions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ namespace ts.Completions {
445445
}
446446

447447
start = timestamp();
448-
const previousToken = findPrecedingToken(position, sourceFile);
448+
const previousToken = findPrecedingToken(position, sourceFile, /*startNode*/ undefined, /*includeJsDoc*/ true);
449449
log("getCompletionData: Get previous token 1: " + (timestamp() - start));
450450

451451
// The decision to provide completion depends on the contextToken, which is determined through the previousToken.
@@ -456,7 +456,7 @@ namespace ts.Completions {
456456
// Skip this partial identifier and adjust the contextToken to the token that precedes it.
457457
if (contextToken && position <= contextToken.end && isWord(contextToken.kind)) {
458458
const start = timestamp();
459-
contextToken = findPrecedingToken(contextToken.getFullStart(), sourceFile);
459+
contextToken = findPrecedingToken(contextToken.getFullStart(), sourceFile, /*startNode*/ undefined, /*includeJsDoc*/ true);
460460
log("getCompletionData: Get previous token 2: " + (timestamp() - start));
461461
}
462462

src/services/services.ts

Lines changed: 38 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ namespace ts {
3737
let ruleProvider: formatting.RulesProvider;
3838

3939
function createNode<TKind extends SyntaxKind>(kind: TKind, pos: number, end: number, parent?: Node): NodeObject | TokenObject<TKind> | IdentifierObject {
40-
const node = kind >= SyntaxKind.FirstNode ? new NodeObject(kind, pos, end) :
40+
const node = isNodeKind(kind) ? new NodeObject(kind, pos, end) :
4141
kind === SyntaxKind.Identifier ? new IdentifierObject(SyntaxKind.Identifier, pos, end) :
4242
new TokenObject(kind, pos, end);
4343
node.parent = parent;
@@ -103,10 +103,10 @@ namespace ts {
103103
return sourceFile.text.substring(this.getStart(sourceFile), this.getEnd());
104104
}
105105

106-
private addSyntheticNodes(nodes: Node[], pos: number, end: number, useJSDocScanner?: boolean): number {
106+
private addSyntheticNodes(nodes: Node[], pos: number, end: number): number {
107107
scanner.setTextPos(pos);
108108
while (pos < end) {
109-
const token = useJSDocScanner ? scanner.scanJSDocToken() : scanner.scan();
109+
const token = scanner.scan();
110110
Debug.assert(token !== SyntaxKind.EndOfFileToken); // Else it would infinitely loop
111111
const textPos = scanner.getTextPos();
112112
if (textPos <= end) {
@@ -136,54 +136,50 @@ namespace ts {
136136
}
137137

138138
private createChildren(sourceFile?: SourceFileLike) {
139-
if (this.kind === SyntaxKind.JSDocComment || isJSDocTag(this)) {
139+
if (!isNodeKind(this.kind)) {
140+
this._children = emptyArray;
141+
return;
142+
}
143+
144+
if (isJSDocCommentContainingNode(this)) {
140145
/** Don't add trivia for "tokens" since this is in a comment. */
141146
const children: Node[] = [];
142147
this.forEachChild(child => { children.push(child); });
143148
this._children = children;
149+
return;
144150
}
145-
else if (this.kind >= SyntaxKind.FirstNode) {
146-
const children: Node[] = [];
147-
scanner.setText((sourceFile || this.getSourceFile()).text);
148-
let pos = this.pos;
149-
const useJSDocScanner = isJSDocNode(this);
150-
const processNode = (node: Node) => {
151-
const isJSDocTagNode = isJSDocNode(node);
152-
if (!isJSDocTagNode && pos < node.pos) {
153-
pos = this.addSyntheticNodes(children, pos, node.pos, useJSDocScanner);
154-
}
155-
children.push(node);
156-
if (!isJSDocTagNode) {
157-
pos = node.end;
158-
}
159-
};
160-
const processNodes = (nodes: NodeArray<Node>) => {
161-
if (pos < nodes.pos) {
162-
pos = this.addSyntheticNodes(children, pos, nodes.pos, useJSDocScanner);
163-
}
164-
children.push(this.createSyntaxList(nodes));
165-
pos = nodes.end;
166-
};
167-
// jsDocComments need to be the first children
168-
if (this.jsDoc) {
169-
for (const jsDocComment of this.jsDoc) {
170-
processNode(jsDocComment);
171-
}
151+
152+
const children: Node[] = [];
153+
scanner.setText((sourceFile || this.getSourceFile()).text);
154+
let pos = this.pos;
155+
const processNode = (node: Node) => {
156+
pos = this.addSyntheticNodes(children, pos, node.pos);
157+
children.push(node);
158+
pos = node.end;
159+
};
160+
const processNodes = (nodes: NodeArray<Node>) => {
161+
if (pos < nodes.pos) {
162+
pos = this.addSyntheticNodes(children, pos, nodes.pos);
172163
}
173-
// For syntactic classifications, all trivia are classcified together, including jsdoc comments.
174-
// For that to work, the jsdoc comments should still be the leading trivia of the first child.
175-
// Restoring the scanner position ensures that.
176-
pos = this.pos;
177-
forEachChild(this, processNode, processNodes);
178-
if (pos < this.end) {
179-
this.addSyntheticNodes(children, pos, this.end);
164+
children.push(this.createSyntaxList(nodes));
165+
pos = nodes.end;
166+
};
167+
// jsDocComments need to be the first children
168+
if (this.jsDoc) {
169+
for (const jsDocComment of this.jsDoc) {
170+
processNode(jsDocComment);
180171
}
181-
scanner.setText(undefined);
182-
this._children = children;
183172
}
184-
else {
185-
this._children = emptyArray;
173+
// For syntactic classifications, all trivia are classcified together, including jsdoc comments.
174+
// For that to work, the jsdoc comments should still be the leading trivia of the first child.
175+
// Restoring the scanner position ensures that.
176+
pos = this.pos;
177+
forEachChild(this, processNode, processNodes);
178+
if (pos < this.end) {
179+
this.addSyntheticNodes(children, pos, this.end);
186180
}
181+
scanner.setText(undefined);
182+
this._children = children;
187183
}
188184

189185
public getChildCount(sourceFile?: SourceFile): number {

src/services/utilities.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,7 @@ namespace ts {
710710
}
711711
}
712712

713-
export function findPrecedingToken(position: number, sourceFile: SourceFile, startNode?: Node): Node {
713+
export function findPrecedingToken(position: number, sourceFile: SourceFile, startNode?: Node, includeJsDoc?: boolean): Node {
714714
return find(startNode || sourceFile);
715715

716716
function findRightmostToken(n: Node): Node {
@@ -741,7 +741,7 @@ namespace ts {
741741
// NOTE: JsxText is a weird kind of node that can contain only whitespaces (since they are not counted as trivia).
742742
// if this is the case - then we should assume that token in question is located in previous child.
743743
if (position < child.end && (nodeHasTokens(child) || child.kind === SyntaxKind.JsxText)) {
744-
const start = child.getStart(sourceFile);
744+
const start = (includeJsDoc && child.jsDoc ? child.jsDoc[0] : child).getStart(sourceFile);
745745
const lookInPreviousChild =
746746
(start >= position) || // cursor in the leading trivia
747747
(child.kind === SyntaxKind.JsxText && start === child.end); // whitespace only JsxText
@@ -758,7 +758,7 @@ namespace ts {
758758
}
759759
}
760760

761-
Debug.assert(startNode !== undefined || n.kind === SyntaxKind.SourceFile);
761+
Debug.assert(startNode !== undefined || n.kind === SyntaxKind.SourceFile || isJSDocCommentContainingNode(n));
762762

763763
// Here we know that none of child token nodes embrace the position,
764764
// the only known case is when position is at the end of the file.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @allowJs: true
4+
5+
// @Filename: /node_modules/foo/index.d.ts
6+
/////** tee */
7+
////export type T = number;
8+
9+
// @Filename: /a.js
10+
////import * as Foo from "foo";
11+
/////** @type {Foo./**/} */
12+
////const x = 0;
13+
14+
goTo.marker();
15+
verify.completionListContains("T", "type T = number", "tee ", "type");

0 commit comments

Comments
 (0)