Skip to content

Commit ecaf16d

Browse files
committed
Merge pull request #4283 from zhengbli/JsDocIntellisense2
Add Intellisense to JsDoc
2 parents c12da5a + 582b0aa commit ecaf16d

File tree

4 files changed

+194
-7
lines changed

4 files changed

+194
-7
lines changed

src/compiler/parser.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5831,7 +5831,6 @@ namespace ts {
58315831

58325832
if (!name) {
58335833
parseErrorAtPosition(pos, 0, Diagnostics.Identifier_expected);
5834-
return undefined;
58355834
}
58365835

58375836
let preName: Identifier, postName: Identifier;

src/services/services.ts

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,46 @@ namespace ts {
132132
let scanner: Scanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ true);
133133

134134
let emptyArray: any[] = [];
135+
136+
const jsDocTagNames = [
137+
"augments",
138+
"author",
139+
"argument",
140+
"borrows",
141+
"class",
142+
"constant",
143+
"constructor",
144+
"constructs",
145+
"default",
146+
"deprecated",
147+
"description",
148+
"event",
149+
"example",
150+
"extends",
151+
"field",
152+
"fileOverview",
153+
"function",
154+
"ignore",
155+
"inner",
156+
"lends",
157+
"link",
158+
"memberOf",
159+
"name",
160+
"namespace",
161+
"param",
162+
"private",
163+
"property",
164+
"public",
165+
"requires",
166+
"returns",
167+
"see",
168+
"since",
169+
"static",
170+
"throws",
171+
"type",
172+
"version"
173+
];
174+
let jsDocCompletionEntries: CompletionEntry[];
135175

136176
function createNode(kind: SyntaxKind, pos: number, end: number, flags: NodeFlags, parent?: Node): NodeObject {
137177
let node = <NodeObject> new (getNodeConstructor(kind))();
@@ -2971,6 +3011,8 @@ namespace ts {
29713011
let sourceFile = getValidSourceFile(fileName);
29723012
let isJavaScriptFile = isJavaScript(fileName);
29733013

3014+
let isJsDocTagName = false;
3015+
29743016
let start = new Date().getTime();
29753017
let currentToken = getTokenAtPosition(sourceFile, position);
29763018
log("getCompletionData: Get current token: " + (new Date().getTime() - start));
@@ -2981,8 +3023,44 @@ namespace ts {
29813023
log("getCompletionData: Is inside comment: " + (new Date().getTime() - start));
29823024

29833025
if (insideComment) {
2984-
log("Returning an empty list because completion was inside a comment.");
2985-
return undefined;
3026+
// The current position is next to the '@' sign, when no tag name being provided yet.
3027+
// Provide a full list of tag names
3028+
if (hasDocComment(sourceFile, position) && sourceFile.text.charCodeAt(position - 1) === CharacterCodes.at) {
3029+
isJsDocTagName = true;
3030+
}
3031+
3032+
// Completion should work inside certain JsDoc tags. For example:
3033+
// /** @type {number | string} */
3034+
// Completion should work in the brackets
3035+
let insideJsDocTagExpression = false;
3036+
let tag = getJsDocTagAtPosition(sourceFile, position);
3037+
if (tag) {
3038+
if (tag.tagName.pos <= position && position <= tag.tagName.end) {
3039+
isJsDocTagName = true;
3040+
}
3041+
3042+
switch (tag.kind) {
3043+
case SyntaxKind.JSDocTypeTag:
3044+
case SyntaxKind.JSDocParameterTag:
3045+
case SyntaxKind.JSDocReturnTag:
3046+
let tagWithExpression = <JSDocTypeTag | JSDocParameterTag | JSDocReturnTag>tag;
3047+
if (tagWithExpression.typeExpression) {
3048+
insideJsDocTagExpression = tagWithExpression.typeExpression.pos < position && position < tagWithExpression.typeExpression.end;
3049+
}
3050+
break;
3051+
}
3052+
}
3053+
3054+
if (isJsDocTagName) {
3055+
return { symbols: undefined, isMemberCompletion: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, isJsDocTagName };
3056+
}
3057+
3058+
if (!insideJsDocTagExpression) {
3059+
// Proceed if the current position is in jsDoc tag expression; otherwise it is a normal
3060+
// comment or the plain text part of a jsDoc comment, so no completion should be available
3061+
log("Returning an empty list because completion was inside a regular comment or plain text part of a JsDoc comment.");
3062+
return undefined;
3063+
}
29863064
}
29873065

29883066
start = new Date().getTime();
@@ -3068,7 +3146,7 @@ namespace ts {
30683146

30693147
log("getCompletionData: Semantic work: " + (new Date().getTime() - semanticStart));
30703148

3071-
return { symbols, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag) };
3149+
return { symbols, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), isJsDocTagName };
30723150

30733151
function getTypeScriptMemberSymbols(): void {
30743152
// Right of dot member completion list
@@ -3708,9 +3786,14 @@ namespace ts {
37083786
return undefined;
37093787
}
37103788

3711-
let { symbols, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot } = completionData;
3789+
let { symbols, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot, isJsDocTagName } = completionData;
37123790

37133791
let entries: CompletionEntry[];
3792+
if (isJsDocTagName) {
3793+
// If the current position is a jsDoc tag name, only tag names should be provided for completion
3794+
return { isMemberCompletion: false, isNewIdentifierLocation: false, entries: getAllJsDocCompletionEntries() };
3795+
}
3796+
37143797
if (isRightOfDot && isJavaScript(fileName)) {
37153798
entries = getCompletionEntriesFromSymbols(symbols);
37163799
addRange(entries, getJavaScriptCompletionEntries());
@@ -3724,7 +3807,7 @@ namespace ts {
37243807
}
37253808

37263809
// Add keywords if this is not a member completion list
3727-
if (!isMemberCompletion) {
3810+
if (!isMemberCompletion && !isJsDocTagName) {
37283811
addRange(entries, keywordCompletions);
37293812
}
37303813

@@ -3757,6 +3840,17 @@ namespace ts {
37573840
return entries;
37583841
}
37593842

3843+
function getAllJsDocCompletionEntries(): CompletionEntry[] {
3844+
return jsDocCompletionEntries || (jsDocCompletionEntries = ts.map(jsDocTagNames, tagName => {
3845+
return {
3846+
name: tagName,
3847+
kind: ScriptElementKind.keyword,
3848+
kindModifiers: "",
3849+
sortText: "0",
3850+
}
3851+
}));
3852+
}
3853+
37603854
function createCompletionEntry(symbol: Symbol, location: Node): CompletionEntry {
37613855
// Try to get a valid display name for this symbol, if we could not find one, then ignore it.
37623856
// We would like to only show things that can be added after a dot, so for instance numeric properties can

src/services/utilities.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,39 @@ namespace ts {
469469
}
470470
}
471471

472+
/**
473+
* Get the corresponding JSDocTag node if the position is in a jsDoc comment
474+
*/
475+
export function getJsDocTagAtPosition(sourceFile: SourceFile, position: number): JSDocTag {
476+
let node = ts.getTokenAtPosition(sourceFile, position);
477+
if (isToken(node)) {
478+
switch (node.kind) {
479+
case SyntaxKind.VarKeyword:
480+
case SyntaxKind.LetKeyword:
481+
case SyntaxKind.ConstKeyword:
482+
// if the current token is var, let or const, skip the VariableDeclarationList
483+
node = node.parent === undefined ? undefined : node.parent.parent;
484+
break;
485+
default:
486+
node = node.parent;
487+
break;
488+
}
489+
}
490+
491+
if (node) {
492+
let jsDocComment = node.jsDocComment;
493+
if (jsDocComment) {
494+
for (let tag of jsDocComment.tags) {
495+
if (tag.pos <= position && position <= tag.end) {
496+
return tag;
497+
}
498+
}
499+
}
500+
}
501+
502+
return undefined;
503+
}
504+
472505
function nodeHasTokens(n: Node): boolean {
473506
// If we have a token or node that has a non-zero width, it must have tokens.
474507
// Note, that getWidth() does not take trivia into account.
@@ -640,7 +673,6 @@ namespace ts {
640673
else if (flags & SymbolFlags.TypeAlias) { return SymbolDisplayPartKind.aliasName; }
641674
else if (flags & SymbolFlags.Alias) { return SymbolDisplayPartKind.aliasName; }
642675

643-
644676
return SymbolDisplayPartKind.text;
645677
}
646678
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
///<reference path="fourslash.ts" />
2+
3+
// @allowNonTsExtensions: true
4+
// @Filename: Foo.js
5+
/////** @/*1*/ */
6+
////var v1;
7+
////
8+
/////** @p/*2*/ */
9+
////var v2;
10+
////
11+
/////** @param /*3*/ */
12+
////var v3;
13+
////
14+
/////** @param { n/*4*/ } bar */
15+
////var v4;
16+
////
17+
/////** @type { n/*5*/ } */
18+
////var v5;
19+
////
20+
////// @/*6*/
21+
////var v6;
22+
////
23+
////// @pa/*7*/
24+
////var v7;
25+
////
26+
/////** @param { n/*8*/ } */
27+
////var v8;
28+
////
29+
/////** @return { n/*9*/ } */
30+
////var v9;
31+
32+
goTo.marker('1');
33+
verify.completionListContains("constructor");
34+
verify.completionListContains("param");
35+
verify.completionListContains("type");
36+
37+
goTo.marker('2');
38+
verify.completionListContains("constructor");
39+
verify.completionListContains("param");
40+
verify.completionListContains("type");
41+
42+
goTo.marker('3');
43+
verify.completionListIsEmpty();
44+
45+
goTo.marker('4');
46+
verify.completionListContains('number');
47+
48+
goTo.marker('5');
49+
verify.completionListContains('number');
50+
51+
goTo.marker('6');
52+
verify.completionListIsEmpty();
53+
54+
goTo.marker('7');
55+
verify.completionListIsEmpty();
56+
57+
goTo.marker('8');
58+
verify.completionListContains('number');
59+
60+
goTo.marker('9');
61+
verify.completionListContains('number');
62+

0 commit comments

Comments
 (0)