2017-03-01 18:28:51 +01:00
|
|
|
/* @internal */
|
2016-09-07 16:04:46 +02:00
|
|
|
namespace ts.JsDoc {
|
2017-11-03 17:53:56 +01:00
|
|
|
const singleLineTemplate = { newText: "/** */", caretOffset: 3 };
|
2016-09-07 16:04:46 +02:00
|
|
|
const jsDocTagNames = [
|
|
|
|
"augments",
|
|
|
|
"author",
|
|
|
|
"argument",
|
|
|
|
"borrows",
|
|
|
|
"class",
|
|
|
|
"constant",
|
|
|
|
"constructor",
|
|
|
|
"constructs",
|
|
|
|
"default",
|
|
|
|
"deprecated",
|
|
|
|
"description",
|
|
|
|
"event",
|
|
|
|
"example",
|
|
|
|
"extends",
|
|
|
|
"field",
|
|
|
|
"fileOverview",
|
|
|
|
"function",
|
|
|
|
"ignore",
|
|
|
|
"inner",
|
|
|
|
"lends",
|
|
|
|
"link",
|
|
|
|
"memberOf",
|
2016-12-14 01:36:54 +01:00
|
|
|
"method",
|
2016-09-07 16:04:46 +02:00
|
|
|
"name",
|
|
|
|
"namespace",
|
|
|
|
"param",
|
|
|
|
"private",
|
2017-05-04 23:16:09 +02:00
|
|
|
"prop",
|
2016-09-07 16:04:46 +02:00
|
|
|
"property",
|
|
|
|
"public",
|
|
|
|
"requires",
|
|
|
|
"returns",
|
|
|
|
"see",
|
|
|
|
"since",
|
|
|
|
"static",
|
|
|
|
"throws",
|
|
|
|
"type",
|
|
|
|
"typedef",
|
|
|
|
"version"
|
|
|
|
];
|
2017-03-01 00:41:35 +01:00
|
|
|
let jsDocTagNameCompletionEntries: CompletionEntry[];
|
2017-03-02 02:46:35 +01:00
|
|
|
let jsDocTagCompletionEntries: CompletionEntry[];
|
2016-09-07 16:04:46 +02:00
|
|
|
|
2017-05-23 18:54:02 +02:00
|
|
|
export function getJsDocCommentsFromDeclarations(declarations?: Declaration[]) {
|
2016-09-15 20:53:04 +02:00
|
|
|
// Only collect doc comments from duplicate declarations once:
|
|
|
|
// In case of a union property there might be same declaration multiple times
|
|
|
|
// which only varies in type parameter
|
|
|
|
// Eg. const a: Array<string> | Array<number>; a.length
|
|
|
|
// The property length will have two declarations of property length coming
|
|
|
|
// from Array<T> - Array<string> and Array<number>
|
2016-09-07 16:04:46 +02:00
|
|
|
const documentationComment = <SymbolDisplayPart[]>[];
|
2016-09-15 20:53:04 +02:00
|
|
|
forEachUnique(declarations, declaration => {
|
2017-07-10 20:26:59 +02:00
|
|
|
forEach(getAllJSDocs(declaration), doc => {
|
|
|
|
if (doc.comment) {
|
2016-09-15 20:53:04 +02:00
|
|
|
if (documentationComment.length) {
|
|
|
|
documentationComment.push(lineBreakPart());
|
2016-09-07 16:04:46 +02:00
|
|
|
}
|
2017-07-10 20:26:59 +02:00
|
|
|
documentationComment.push(textPart(doc.comment));
|
2016-09-07 16:04:46 +02:00
|
|
|
}
|
2017-07-10 20:26:59 +02:00
|
|
|
});
|
2016-09-15 20:53:04 +02:00
|
|
|
});
|
|
|
|
return documentationComment;
|
|
|
|
}
|
2016-09-07 16:04:46 +02:00
|
|
|
|
2017-10-30 18:27:19 +01:00
|
|
|
export function getJsDocTagsFromDeclarations(declarations?: Declaration[]): JSDocTagInfo[] {
|
2016-12-13 00:29:29 +01:00
|
|
|
// Only collect doc comments from duplicate declarations once.
|
|
|
|
const tags: JSDocTagInfo[] = [];
|
|
|
|
forEachUnique(declarations, declaration => {
|
2017-07-10 20:26:59 +02:00
|
|
|
for (const tag of getJSDocTags(declaration)) {
|
2017-10-30 18:27:19 +01:00
|
|
|
tags.push({ name: tag.tagName.text, text: getCommentText(tag) });
|
2016-12-13 00:29:29 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
return tags;
|
|
|
|
}
|
|
|
|
|
2017-10-30 18:27:19 +01:00
|
|
|
function getCommentText(tag: JSDocTag): string {
|
|
|
|
const { comment } = tag;
|
|
|
|
switch (tag.kind) {
|
|
|
|
case SyntaxKind.JSDocAugmentsTag:
|
|
|
|
return withNode((tag as JSDocAugmentsTag).class);
|
|
|
|
case SyntaxKind.JSDocTemplateTag:
|
|
|
|
return withList((tag as JSDocTemplateTag).typeParameters);
|
|
|
|
case SyntaxKind.JSDocTypeTag:
|
|
|
|
return withNode((tag as JSDocTypeTag).typeExpression);
|
|
|
|
case SyntaxKind.JSDocTypedefTag:
|
|
|
|
case SyntaxKind.JSDocPropertyTag:
|
|
|
|
case SyntaxKind.JSDocParameterTag:
|
|
|
|
const { name } = tag as JSDocTypedefTag | JSDocPropertyTag | JSDocParameterTag;
|
|
|
|
return name ? withNode(name) : comment;
|
|
|
|
default:
|
|
|
|
return comment;
|
|
|
|
}
|
|
|
|
|
|
|
|
function withNode(node: Node) {
|
|
|
|
return `${node.getText()} ${comment}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
function withList(list: NodeArray<Node>): string {
|
|
|
|
return `${list.map(x => x.getText())} ${comment}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-15 20:53:04 +02:00
|
|
|
/**
|
|
|
|
* Iterates through 'array' by index and performs the callback on each element of array until the callback
|
|
|
|
* returns a truthy value, then returns that value.
|
|
|
|
* If no such value is found, the callback is applied to each element of array and undefined is returned.
|
|
|
|
*/
|
|
|
|
function forEachUnique<T, U>(array: T[], callback: (element: T, index: number) => U): U {
|
|
|
|
if (array) {
|
2016-12-19 19:12:35 +01:00
|
|
|
for (let i = 0; i < array.length; i++) {
|
2016-09-15 20:53:04 +02:00
|
|
|
if (indexOf(array, array[i]) === i) {
|
|
|
|
const result = callback(array[i], i);
|
|
|
|
if (result) {
|
|
|
|
return result;
|
2016-09-07 16:04:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-09-15 20:53:04 +02:00
|
|
|
return undefined;
|
2016-09-07 16:04:46 +02:00
|
|
|
}
|
|
|
|
|
2017-03-02 02:46:35 +01:00
|
|
|
export function getJSDocTagNameCompletions(): CompletionEntry[] {
|
|
|
|
return jsDocTagNameCompletionEntries || (jsDocTagNameCompletionEntries = ts.map(jsDocTagNames, tagName => {
|
2016-09-07 16:04:46 +02:00
|
|
|
return {
|
|
|
|
name: tagName,
|
|
|
|
kind: ScriptElementKind.keyword,
|
|
|
|
kindModifiers: "",
|
|
|
|
sortText: "0",
|
|
|
|
};
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2017-10-26 19:58:33 +02:00
|
|
|
export const getJSDocTagNameCompletionDetails = getJSDocTagCompletionDetails;
|
|
|
|
|
2017-03-02 02:46:35 +01:00
|
|
|
export function getJSDocTagCompletions(): CompletionEntry[] {
|
|
|
|
return jsDocTagCompletionEntries || (jsDocTagCompletionEntries = ts.map(jsDocTagNames, tagName => {
|
|
|
|
return {
|
|
|
|
name: `@${tagName}`,
|
|
|
|
kind: ScriptElementKind.keyword,
|
|
|
|
kindModifiers: "",
|
|
|
|
sortText: "0"
|
2017-03-03 16:00:52 +01:00
|
|
|
};
|
2017-03-02 02:46:35 +01:00
|
|
|
}));
|
2016-09-07 16:04:46 +02:00
|
|
|
}
|
|
|
|
|
2017-10-26 19:58:33 +02:00
|
|
|
export function getJSDocTagCompletionDetails(name: string): CompletionEntryDetails {
|
|
|
|
return {
|
|
|
|
name,
|
|
|
|
kind: ScriptElementKind.unknown, // TODO: should have its own kind?
|
|
|
|
kindModifiers: "",
|
|
|
|
displayParts: [textPart(name)],
|
|
|
|
documentation: emptyArray,
|
|
|
|
tags: emptyArray,
|
|
|
|
codeActions: undefined,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-06-07 21:28:52 +02:00
|
|
|
export function getJSDocParameterNameCompletions(tag: JSDocParameterTag): CompletionEntry[] {
|
2017-07-26 19:57:29 +02:00
|
|
|
if (!isIdentifier(tag.name)) {
|
|
|
|
return emptyArray;
|
|
|
|
}
|
2017-07-25 22:16:34 +02:00
|
|
|
const nameThusFar = tag.name.text;
|
2017-06-07 21:28:52 +02:00
|
|
|
const jsdoc = tag.parent;
|
|
|
|
const fn = jsdoc.parent;
|
|
|
|
if (!ts.isFunctionLike(fn)) return [];
|
|
|
|
|
|
|
|
return mapDefined(fn.parameters, param => {
|
|
|
|
if (!isIdentifier(param.name)) return undefined;
|
|
|
|
|
2017-07-25 22:16:34 +02:00
|
|
|
const name = param.name.text;
|
2017-07-26 20:09:24 +02:00
|
|
|
if (jsdoc.tags.some(t => t !== tag && isJSDocParameterTag(t) && isIdentifier(t.name) && t.name.escapedText === name)
|
2017-06-08 00:50:26 +02:00
|
|
|
|| nameThusFar !== undefined && !startsWith(name, nameThusFar)) {
|
2017-06-07 21:28:52 +02:00
|
|
|
return undefined;
|
2017-06-08 00:50:26 +02:00
|
|
|
}
|
2017-06-07 21:28:52 +02:00
|
|
|
|
|
|
|
return { name, kind: ScriptElementKind.parameterElement, kindModifiers: "", sortText: "0" };
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-10-26 19:58:33 +02:00
|
|
|
export function getJSDocParameterNameCompletionDetails(name: string): CompletionEntryDetails {
|
|
|
|
return {
|
|
|
|
name,
|
|
|
|
kind: ScriptElementKind.parameterElement,
|
|
|
|
kindModifiers: "",
|
|
|
|
displayParts: [textPart(name)],
|
|
|
|
documentation: emptyArray,
|
|
|
|
tags: emptyArray,
|
|
|
|
codeActions: undefined,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2016-09-07 16:04:46 +02:00
|
|
|
/**
|
|
|
|
* Checks if position points to a valid position to add JSDoc comments, and if so,
|
|
|
|
* returns the appropriate template. Otherwise returns an empty string.
|
2017-11-03 17:53:56 +01:00
|
|
|
* Invalid positions are
|
|
|
|
* - within comments, strings (including template literals and regex), and JSXText
|
|
|
|
* - within a token
|
2016-09-07 16:04:46 +02:00
|
|
|
*
|
|
|
|
* Hosts should ideally check that:
|
|
|
|
* - The line is all whitespace up to 'position' before performing the insertion.
|
|
|
|
* - If the keystroke sequence "/\*\*" induced the call, we also check that the next
|
|
|
|
* non-whitespace character is '*', which (approximately) indicates whether we added
|
|
|
|
* the second '*' to complete an existing (JSDoc) comment.
|
|
|
|
* @param fileName The file in which to perform the check.
|
|
|
|
* @param position The (character-indexed) position in the file where the check should
|
|
|
|
* be performed.
|
|
|
|
*/
|
2017-11-02 19:08:26 +01:00
|
|
|
|
|
|
|
export function getDocCommentTemplateAtPosition(newLine: string, sourceFile: SourceFile, position: number): TextInsertion | undefined {
|
2016-09-07 16:04:46 +02:00
|
|
|
// Check if in a context where we don't want to perform any insertion
|
|
|
|
if (isInString(sourceFile, position) || isInComment(sourceFile, position) || hasDocComment(sourceFile, position)) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2017-05-12 17:33:31 +02:00
|
|
|
const tokenAtPos = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false);
|
2016-09-07 16:04:46 +02:00
|
|
|
const tokenStart = tokenAtPos.getStart();
|
|
|
|
if (!tokenAtPos || tokenStart < position) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2017-09-07 16:21:47 +02:00
|
|
|
const commentOwnerInfo = getCommentOwnerInfo(tokenAtPos);
|
|
|
|
if (!commentOwnerInfo) {
|
2017-11-03 17:53:56 +01:00
|
|
|
// if climbing the tree did not find a declaration with parameters, complete to a single line comment
|
|
|
|
return singleLineTemplate;
|
2016-09-07 16:04:46 +02:00
|
|
|
}
|
2017-09-07 16:21:47 +02:00
|
|
|
const { commentOwner, parameters } = commentOwnerInfo;
|
2017-11-03 17:53:56 +01:00
|
|
|
|
|
|
|
if (commentOwner.kind === SyntaxKind.JsxText) {
|
2016-09-07 16:04:46 +02:00
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2017-11-03 19:19:53 +01:00
|
|
|
if (commentOwner.getStart() < position || parameters.length === 0) {
|
|
|
|
// if climbing the tree found a declaration with parameters but the request was made inside it
|
|
|
|
// or if there are no parameters, complete to a single line comment
|
2017-11-03 17:53:56 +01:00
|
|
|
return singleLineTemplate;
|
2017-11-02 17:55:56 +01:00
|
|
|
}
|
|
|
|
|
2016-09-07 16:04:46 +02:00
|
|
|
const posLineAndChar = sourceFile.getLineAndCharacterOfPosition(position);
|
|
|
|
const lineStart = sourceFile.getLineStarts()[posLineAndChar.line];
|
|
|
|
|
2017-03-08 21:41:20 +01:00
|
|
|
// replace non-whitespace characters in prefix with spaces.
|
|
|
|
const indentationStr = sourceFile.text.substr(lineStart, posLineAndChar.character).replace(/\S/i, () => " ");
|
2016-12-08 19:07:11 +01:00
|
|
|
const isJavaScriptFile = hasJavaScriptFileExtension(sourceFile.fileName);
|
2016-09-07 16:04:46 +02:00
|
|
|
|
2017-11-03 19:19:53 +01:00
|
|
|
const docParams = parameters.map(({name}, i) => {
|
|
|
|
const nameText = isIdentifier(name) ? name.text : `param${i}`;
|
|
|
|
const type = isJavaScriptFile ? "{any} " : "";
|
|
|
|
return `${indentationStr} * @param ${type}${nameText}${newLine}`;
|
|
|
|
}).join("");
|
2016-09-07 16:04:46 +02:00
|
|
|
|
|
|
|
// A doc comment consists of the following
|
|
|
|
// * The opening comment line
|
|
|
|
// * the first line (without a param) for the object's untagged info (this is also where the caret ends up)
|
|
|
|
// * the '@param'-tagged lines
|
|
|
|
// * TODO: other tags.
|
|
|
|
// * the closing comment line
|
|
|
|
// * if the caret was directly in front of the object, then we add an extra line and indentation.
|
|
|
|
const preamble = "/**" + newLine +
|
|
|
|
indentationStr + " * ";
|
|
|
|
const result =
|
|
|
|
preamble + newLine +
|
|
|
|
docParams +
|
|
|
|
indentationStr + " */" +
|
|
|
|
(tokenStart === position ? newLine + indentationStr : "");
|
|
|
|
|
|
|
|
return { newText: result, caretOffset: preamble.length };
|
|
|
|
}
|
|
|
|
|
2017-09-07 16:21:47 +02:00
|
|
|
interface CommentOwnerInfo {
|
|
|
|
readonly commentOwner: Node;
|
2017-11-03 17:53:56 +01:00
|
|
|
readonly parameters: ReadonlyArray<ParameterDeclaration>;
|
2017-09-07 16:21:47 +02:00
|
|
|
}
|
|
|
|
function getCommentOwnerInfo(tokenAtPos: Node): CommentOwnerInfo | undefined {
|
|
|
|
for (let commentOwner = tokenAtPos; commentOwner; commentOwner = commentOwner.parent) {
|
|
|
|
switch (commentOwner.kind) {
|
|
|
|
case SyntaxKind.FunctionDeclaration:
|
|
|
|
case SyntaxKind.MethodDeclaration:
|
|
|
|
case SyntaxKind.Constructor:
|
2017-10-28 01:46:39 +02:00
|
|
|
case SyntaxKind.MethodSignature:
|
|
|
|
const { parameters } = commentOwner as FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | MethodSignature;
|
2017-09-07 16:21:47 +02:00
|
|
|
return { commentOwner, parameters };
|
2016-09-07 16:04:46 +02:00
|
|
|
|
2017-09-07 16:21:47 +02:00
|
|
|
case SyntaxKind.VariableStatement: {
|
|
|
|
const varStatement = <VariableStatement>commentOwner;
|
|
|
|
const varDeclarations = varStatement.declarationList.declarations;
|
|
|
|
const parameters = varDeclarations.length === 1 && varDeclarations[0].initializer
|
|
|
|
? getParametersFromRightHandSideOfAssignment(varDeclarations[0].initializer)
|
|
|
|
: undefined;
|
2017-11-03 17:53:56 +01:00
|
|
|
return parameters ? { commentOwner, parameters } : undefined;
|
2017-09-07 16:21:47 +02:00
|
|
|
}
|
2016-09-07 16:04:46 +02:00
|
|
|
|
2017-09-07 16:21:47 +02:00
|
|
|
case SyntaxKind.SourceFile:
|
|
|
|
return undefined;
|
|
|
|
|
|
|
|
case SyntaxKind.BinaryExpression: {
|
|
|
|
const be = commentOwner as BinaryExpression;
|
|
|
|
if (getSpecialPropertyAssignmentKind(be) === ts.SpecialPropertyAssignmentKind.None) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
const parameters = isFunctionLike(be.right) ? be.right.parameters : emptyArray;
|
|
|
|
return { commentOwner, parameters };
|
|
|
|
}
|
2017-11-03 17:53:56 +01:00
|
|
|
|
|
|
|
case SyntaxKind.JsxText: {
|
|
|
|
const parameters: ReadonlyArray<ParameterDeclaration> = emptyArray;
|
|
|
|
return { commentOwner, parameters };
|
|
|
|
}
|
2016-09-07 16:04:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Digs into an an initializer or RHS operand of an assignment operation
|
|
|
|
* to get the parameters of an apt signature corresponding to a
|
|
|
|
* function expression or a class expression.
|
|
|
|
*
|
|
|
|
* @param rightHandSide the expression which may contain an appropriate set of parameters
|
|
|
|
* @returns the parameters of a signature found on the RHS if one exists; otherwise 'emptyArray'.
|
|
|
|
*/
|
2017-07-18 19:38:21 +02:00
|
|
|
function getParametersFromRightHandSideOfAssignment(rightHandSide: Expression): ReadonlyArray<ParameterDeclaration> {
|
2016-09-07 16:04:46 +02:00
|
|
|
while (rightHandSide.kind === SyntaxKind.ParenthesizedExpression) {
|
|
|
|
rightHandSide = (<ParenthesizedExpression>rightHandSide).expression;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (rightHandSide.kind) {
|
|
|
|
case SyntaxKind.FunctionExpression:
|
|
|
|
case SyntaxKind.ArrowFunction:
|
|
|
|
return (<FunctionExpression>rightHandSide).parameters;
|
|
|
|
case SyntaxKind.ClassExpression:
|
|
|
|
for (const member of (<ClassExpression>rightHandSide).members) {
|
|
|
|
if (member.kind === SyntaxKind.Constructor) {
|
|
|
|
return (<ConstructorDeclaration>member).parameters;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return emptyArray;
|
|
|
|
}
|
|
|
|
}
|