Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5831,7 +5831,6 @@ namespace ts {

if (!name) {
parseErrorAtPosition(pos, 0, Diagnostics.Identifier_expected);
return undefined;
}

let preName: Identifier, postName: Identifier;
Expand Down
104 changes: 99 additions & 5 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,46 @@ namespace ts {
let scanner: Scanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ true);

let emptyArray: any[] = [];

const jsDocTagNames = [
"augments",
"author",
"argument",
"borrows",
"class",
"constant",
"constructor",
"constructs",
"default",
"deprecated",
"description",
"event",
"example",
"extends",
"field",
"fileOverview",
"function",
"ignore",
"inner",
"lends",
"link",
"memberOf",
"name",
"namespace",
"param",
"private",
"property",
"public",
"requires",
"returns",
"see",
"since",
"static",
"throws",
"type",
"version"
];
let jsDocCompletionEntries: CompletionEntry[];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if you could just calculate it regardless of whether you're in a .js file. Check with @mhegazy and @CyrusNajmabadi.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean "calculate" it? For now the main difference between js file and ts file is that node.jsDocComment is only useful in js files

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean that if you're going to just map over it once, do it here regardless of whether it's in a TypeScript or JavaScript file. It would be easier to read than the current lazy initialization, but the way it's done here is fine too.


function createNode(kind: SyntaxKind, pos: number, end: number, flags: NodeFlags, parent?: Node): NodeObject {
let node = <NodeObject> new (getNodeConstructor(kind))();
Expand Down Expand Up @@ -2918,6 +2958,8 @@ namespace ts {
let sourceFile = getValidSourceFile(fileName);
let isJavaScriptFile = isJavaScript(fileName);

let isJsDocTagName = false;

let start = new Date().getTime();
let currentToken = getTokenAtPosition(sourceFile, position);
log("getCompletionData: Get current token: " + (new Date().getTime() - start));
Expand All @@ -2928,8 +2970,44 @@ namespace ts {
log("getCompletionData: Is inside comment: " + (new Date().getTime() - start));

if (insideComment) {
log("Returning an empty list because completion was inside a comment.");
return undefined;
// The current position is next to the '@' sign, when no tag name being provided yet.
// Provide a full list of tag names
if (hasDocComment(sourceFile, position) && sourceFile.text.charCodeAt(position - 1) === CharacterCodes.at) {
isJsDocTagName = true;
}

// Completion should work inside certain JsDoc tags. For example:
// /** @type {number | string} */
// Completion should work in the brackets
let insideJsDocTagExpression = false;
let tag = getJsDocTagAtPosition(sourceFile, position);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this logic should be in getJsDocCompletionEntries instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just get the previous token and see if it's the @ token. Similar to how we look back to see if we're after a dot token?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the previous token I got was the one in code, not in the comment. Or I can modify the getChildren and other related functions, if that is preferred solution

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might be preferred, though I'll defer to Cyrus on this one.

if (tag) {
if (tag.tagName.pos <= position && position <= tag.tagName.end) {
isJsDocTagName = true;
}

switch (tag.kind) {
case SyntaxKind.JSDocTypeTag:
case SyntaxKind.JSDocParameterTag:
case SyntaxKind.JSDocReturnTag:
let tagWithExpression = <JSDocTypeTag | JSDocParameterTag | JSDocReturnTag>tag;
if (tagWithExpression.typeExpression) {
insideJsDocTagExpression = tagWithExpression.typeExpression.pos < position && position < tagWithExpression.typeExpression.end;
}
break;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These branches are identical, just let each fall through and use a variable typeTagOrParamTag of type JSDocTypeTag | JSDocParamTag.

}
}

if (isJsDocTagName) {
return { symbols: undefined, isMemberCompletion: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, isJsDocTagName };
}

if (!insideJsDocTagExpression) {
// Proceed if the current position is in jsDoc tag expression; otherwise it is a normal
// comment or the plain text part of a jsDoc comment, so no completion should be available
log("Returning an empty list because completion was inside a regular comment or plain text part of a JsDoc comment.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want normal intellisense here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? It is inside either a normal comment or the plain text part of a JsDoc

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/**
  * @type { | } 
  */

completion there should show you all global types, e.g. string.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, though that is when insideJsDocTagExpression is true. The commented line is when the cursor is out of the braces, e.g.

/**
 * @type { } |
 */

return undefined;
}
}

start = new Date().getTime();
Expand Down Expand Up @@ -3015,7 +3093,7 @@ namespace ts {

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

return { symbols, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag) };
return { symbols, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), isJsDocTagName };

function getTypeScriptMemberSymbols(): void {
// Right of dot member completion list
Expand Down Expand Up @@ -3656,9 +3734,14 @@ namespace ts {
return undefined;
}

let { symbols, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot } = completionData;
let { symbols, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot, isJsDocTagName } = completionData;

let entries: CompletionEntry[];
if (isJsDocTagName) {
// If the current position is a jsDoc tag name, only tag names should be provided for completion
return { isMemberCompletion: false, isNewIdentifierLocation: false, entries: getAllJsDocCompletionEntries() };
}

if (isRightOfDot && isJavaScript(fileName)) {
entries = getCompletionEntriesFromSymbols(symbols);
addRange(entries, getJavaScriptCompletionEntries());
Expand All @@ -3672,7 +3755,7 @@ namespace ts {
}

// Add keywords if this is not a member completion list
if (!isMemberCompletion) {
if (!isMemberCompletion && !isJsDocTagName) {
addRange(entries, keywordCompletions);
}

Expand Down Expand Up @@ -3705,6 +3788,17 @@ namespace ts {
return entries;
}

function getAllJsDocCompletionEntries(): CompletionEntry[] {
return jsDocCompletionEntries || (jsDocCompletionEntries = ts.map(jsDocTagNames, tagName => {
return {
name: tagName,
kind: ScriptElementKind.keyword,
kindModifiers: "",
sortText: "0",
}
}));
}

function createCompletionEntry(symbol: Symbol, location: Node): CompletionEntry {
// Try to get a valid display name for this symbol, if we could not find one, then ignore it.
// We would like to only show things that can be added after a dot, so for instance numeric properties can
Expand Down
34 changes: 33 additions & 1 deletion src/services/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,39 @@ namespace ts {
}
}

/**
* Get the corresponding JSDocTag node if the position is in a jsDoc comment
*/
export function getJsDocTagAtPosition(sourceFile: SourceFile, position: number): JSDocTag {
let node = ts.getTokenAtPosition(sourceFile, position);
if (isToken(node)) {
switch (node.kind) {
case SyntaxKind.VarKeyword:
case SyntaxKind.LetKeyword:
case SyntaxKind.ConstKeyword:
// if the current token is var, let or const, skip the VariableDeclarationList
node = node.parent === undefined ? undefined : node.parent.parent;
break;
default:
node = node.parent;
break;
}
}

if (node) {
let jsDocComment = node.jsDocComment;
if (jsDocComment) {
for (let tag of jsDocComment.tags) {
if (tag.pos <= position && position <= tag.end) {
return tag;
}
}
}
}

return undefined;
}

function nodeHasTokens(n: Node): boolean {
// If we have a token or node that has a non-zero width, it must have tokens.
// Note, that getWidth() does not take trivia into account.
Expand Down Expand Up @@ -640,7 +673,6 @@ namespace ts {
else if (flags & SymbolFlags.TypeAlias) { return SymbolDisplayPartKind.aliasName; }
else if (flags & SymbolFlags.Alias) { return SymbolDisplayPartKind.aliasName; }


return SymbolDisplayPartKind.text;
}
}
Expand Down
62 changes: 62 additions & 0 deletions tests/cases/fourslash/completionInJsDoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
///<reference path="fourslash.ts" />

// @allowNonTsExtensions: true
// @Filename: Foo.js
/////** @/*1*/ */
////var v1;
////
/////** @p/*2*/ */
////var v2;
////
/////** @param /*3*/ */
////var v3;
////
/////** @param { n/*4*/ } bar */
////var v4;
////
/////** @type { n/*5*/ } */
////var v5;
////
////// @/*6*/
////var v6;
////
////// @pa/*7*/
////var v7;
////
/////** @param { n/*8*/ } */
////var v8;
////
/////** @return { n/*9*/ } */
////var v9;

goTo.marker('1');
verify.completionListContains("constructor");
verify.completionListContains("param");
verify.completionListContains("type");

goTo.marker('2');
verify.completionListContains("constructor");
verify.completionListContains("param");
verify.completionListContains("type");

goTo.marker('3');
verify.completionListIsEmpty();

goTo.marker('4');
verify.completionListContains('number');

goTo.marker('5');
verify.completionListContains('number');

goTo.marker('6');
verify.completionListIsEmpty();

goTo.marker('7');
verify.completionListIsEmpty();

goTo.marker('8');
verify.completionListContains('number');

goTo.marker('9');
verify.completionListContains('number');