Merge pull request #4283 from zhengbli/JsDocIntellisense2

Add Intellisense to JsDoc
This commit is contained in:
Zhengbo Li 2015-08-21 16:08:37 -07:00
commit ecaf16df4b
4 changed files with 194 additions and 7 deletions

View file

@ -5831,7 +5831,6 @@ namespace ts {
if (!name) {
parseErrorAtPosition(pos, 0, Diagnostics.Identifier_expected);
return undefined;
}
let preName: Identifier, postName: Identifier;

View file

@ -132,6 +132,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[];
function createNode(kind: SyntaxKind, pos: number, end: number, flags: NodeFlags, parent?: Node): NodeObject {
let node = <NodeObject> new (getNodeConstructor(kind))();
@ -2971,6 +3011,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));
@ -2981,8 +3023,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);
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;
}
}
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.");
return undefined;
}
}
start = new Date().getTime();
@ -3068,7 +3146,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
@ -3708,9 +3786,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());
@ -3724,7 +3807,7 @@ namespace ts {
}
// Add keywords if this is not a member completion list
if (!isMemberCompletion) {
if (!isMemberCompletion && !isJsDocTagName) {
addRange(entries, keywordCompletions);
}
@ -3757,6 +3840,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

View file

@ -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.
@ -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;
}
}

View file

@ -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');