mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Editor support for link tag (#41877)
* Initial scribbles
* Compiles but provides spans instead of location pairs
Probably need to fork the services/server types and provide a conversion
with Session.toFileSpan. Not sure where to put the conversion.
* Switch to DocumentSpan
In theory this is already better supported, but not sure practise bears
that out.
* Builds w/protocol types + conversions
* cleanup:better names and scrub TODOs
* fix test harness too
* Misc
1. Simplify protocol after talking to @mjbvz.
2. Add more tests.
3. Initial notes about where to add parsing.
* Parse and store links in the compiler
The text of the link is still stored in the comment text, but that's now
kept in an object instead of just a string. Each link has the parse for
the entity reference, if there is one.
Needs lots more tests -- this just makes all the existing jsdoc tests
pass.
* more tests and some fixes
* Fix other failing tests
* fix bad merge
* polish parser
* improve names and array types
* slight tweaks
* remove some done TODOs
* more tests + resulting fixes
* add+fix cross-module tests
* Support `@see {@link`
Plus find-all-refs support equivalent to @see's.
* add server test
* Make comments actually part of the AST
* Add span for link.name in language service/protocol
* Make checker optional in getJSDocTags
Also change to JSDocCommentText from JSDocCommentComment
* Use getTokenValue instead of getTokenText
Measure twice, slice once
* Add missing support for top-level links
The language service and protocol were missing support for top-level
links. This commit adds that plumbing.
* add string back to comment type in node constructors
* Full parse of link tags and jsdoc comment text
- Doesn't pass fourslash yet, I'm going to switch to baselines for
failures there.
- Still needs some work on the protocol to convert file+offset to
file+line+offset.
* fix lint
* Fix missing newlines in inferFromUsage codefix
* Parse jsdoc comments as text node/link array
And switch to line+character offsets in the protocol
* Fix fourslash tests
Mostly ones that can't be baselined, but I switched a couple more over
to baselines
* Improve types and documentation
* Test+fix @link emit, scrub other TODOs
* update API baselines
* test that goto-def works with @link
* Split link displaypart into 3
One for link prefix and suffix, one for link name, and one for link
text.
* update baselines
* Provide JSDocTagInfo.text: string to full clients by default
Instead of upgrading them to displayparts.
* Real server tests
* Disambiguate {@link} and @param x {type}
They are ambiguous; previously the parser preferred the type
interpretation, but will now look ahead and parse links instead when the
prefix is `{@link`.
* Add explanatory comment in test
* fix location in richResponse in protocol
* update API baseline
* Address PR comments
1. Add a cross-file goto-def test.
2. Switch from per-message args to UserPreference.
* use arraysEqual from core
This commit is contained in:
@@ -740,6 +740,9 @@ namespace ts {
|
||||
pos = tag.end;
|
||||
break;
|
||||
}
|
||||
if (tag.comment) {
|
||||
pushCommentRange(tag.comment.pos, tag.comment.end - tag.comment.pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ namespace ts.codefix {
|
||||
const typeNode = getTypeNodeIfAccessible(type, parent, program, host);
|
||||
if (typeNode) {
|
||||
// Note that the codefix will never fire with an existing `@type` tag, so there is no need to merge tags
|
||||
const typeTag = factory.createJSDocTypeTag(/*tagName*/ undefined, factory.createJSDocTypeExpression(typeNode), /*comment*/ "");
|
||||
const typeTag = factory.createJSDocTypeTag(/*tagName*/ undefined, factory.createJSDocTypeExpression(typeNode), /*comment*/ undefined);
|
||||
addJSDocTags(changes, sourceFile, cast(parent.parent.parent, isExpressionStatement), [typeTag]);
|
||||
}
|
||||
importAdder.writeFixes(changes);
|
||||
@@ -307,7 +307,7 @@ namespace ts.codefix {
|
||||
return;
|
||||
}
|
||||
const typeExpression = factory.createJSDocTypeExpression(typeNode);
|
||||
const typeTag = isGetAccessorDeclaration(declaration) ? factory.createJSDocReturnTag(/*tagName*/ undefined, typeExpression, "") : factory.createJSDocTypeTag(/*tagName*/ undefined, typeExpression, "");
|
||||
const typeTag = isGetAccessorDeclaration(declaration) ? factory.createJSDocReturnTag(/*tagName*/ undefined, typeExpression, /*comment*/ undefined) : factory.createJSDocTypeTag(/*tagName*/ undefined, typeExpression, /*comment*/ undefined);
|
||||
addJSDocTags(changes, sourceFile, parent, [typeTag]);
|
||||
}
|
||||
else if (!tryReplaceImportTypeNodeWithAutoImport(typeNode, declaration, sourceFile, changes, importAdder, getEmitScriptTarget(program.getCompilerOptions()))) {
|
||||
@@ -374,20 +374,20 @@ namespace ts.codefix {
|
||||
}
|
||||
else {
|
||||
const paramTags = map(inferences, ({ name, typeNode, isOptional }) =>
|
||||
factory.createJSDocParameterTag(/*tagName*/ undefined, name, /*isBracketed*/ !!isOptional, factory.createJSDocTypeExpression(typeNode), /* isNameFirst */ false, ""));
|
||||
factory.createJSDocParameterTag(/*tagName*/ undefined, name, /*isBracketed*/ !!isOptional, factory.createJSDocTypeExpression(typeNode), /* isNameFirst */ false, /*comment*/ undefined));
|
||||
addJSDocTags(changes, sourceFile, signature, paramTags);
|
||||
}
|
||||
}
|
||||
|
||||
export function addJSDocTags(changes: textChanges.ChangeTracker, sourceFile: SourceFile, parent: HasJSDoc, newTags: readonly JSDocTag[]): void {
|
||||
const comments = mapDefined(parent.jsDoc, j => j.comment);
|
||||
const comments = flatMap(parent.jsDoc, j => j.comment) as (JSDocText | JSDocLink)[];
|
||||
const oldTags = flatMapToMutable(parent.jsDoc, j => j.tags);
|
||||
const unmergedNewTags = newTags.filter(newTag => !oldTags || !oldTags.some((tag, i) => {
|
||||
const merged = tryMergeJsdocTags(tag, newTag);
|
||||
if (merged) oldTags[i] = merged;
|
||||
return !!merged;
|
||||
}));
|
||||
const tag = factory.createJSDocComment(comments.join("\n"), factory.createNodeArray([...(oldTags || emptyArray), ...unmergedNewTags]));
|
||||
const tag = factory.createJSDocComment(factory.createNodeArray(intersperse(comments, factory.createJSDocText("\n"))), factory.createNodeArray([...(oldTags || emptyArray), ...unmergedNewTags]));
|
||||
const jsDocNode = parent.kind === SyntaxKind.ArrowFunction ? getJsDocNodeForArrowFunction(parent) : parent;
|
||||
jsDocNode.jsDoc = parent.jsDoc;
|
||||
jsDocNode.jsDocCache = parent.jsDocCache;
|
||||
|
||||
+25
-15
@@ -82,21 +82,28 @@ namespace ts.JsDoc {
|
||||
let jsDocTagNameCompletionEntries: CompletionEntry[];
|
||||
let jsDocTagCompletionEntries: CompletionEntry[];
|
||||
|
||||
export function getJsDocCommentsFromDeclarations(declarations: readonly Declaration[]): SymbolDisplayPart[] {
|
||||
export function getJsDocCommentsFromDeclarations(declarations: readonly Declaration[], checker?: TypeChecker): SymbolDisplayPart[] {
|
||||
// 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>
|
||||
const documentationComment: string[] = [];
|
||||
const parts: SymbolDisplayPart[][] = [];
|
||||
forEachUnique(declarations, declaration => {
|
||||
for (const { comment } of getCommentHavingNodes(declaration)) {
|
||||
if (comment === undefined) continue;
|
||||
pushIfUnique(documentationComment, comment);
|
||||
const newparts = getDisplayPartsFromComment(comment, checker);
|
||||
if (!contains(parts, newparts, isIdenticalListOfDisplayParts)) {
|
||||
parts.push(newparts);
|
||||
}
|
||||
}
|
||||
});
|
||||
return intersperse(map(documentationComment, textPart), lineBreakPart());
|
||||
return flatten(intersperse(parts, [lineBreakPart()]));
|
||||
}
|
||||
|
||||
function isIdenticalListOfDisplayParts(parts1: SymbolDisplayPart[], parts2: SymbolDisplayPart[]) {
|
||||
return arraysEqual(parts1, parts2, (p1, p2) => p1.kind === p2.kind && p1.text === p2.text);
|
||||
}
|
||||
|
||||
function getCommentHavingNodes(declaration: Declaration): readonly (JSDoc | JSDocTag)[] {
|
||||
@@ -112,18 +119,25 @@ namespace ts.JsDoc {
|
||||
}
|
||||
}
|
||||
|
||||
export function getJsDocTagsFromDeclarations(declarations?: Declaration[]): JSDocTagInfo[] {
|
||||
export function getJsDocTagsFromDeclarations(declarations?: Declaration[], checker?: TypeChecker): JSDocTagInfo[] {
|
||||
// Only collect doc comments from duplicate declarations once.
|
||||
const tags: JSDocTagInfo[] = [];
|
||||
forEachUnique(declarations, declaration => {
|
||||
for (const tag of getJSDocTags(declaration)) {
|
||||
tags.push({ name: tag.tagName.text, text: getCommentText(tag) });
|
||||
tags.push({ name: tag.tagName.text, text: getCommentDisplayParts(tag, checker) });
|
||||
}
|
||||
});
|
||||
return tags;
|
||||
}
|
||||
|
||||
function getCommentText(tag: JSDocTag): string | undefined {
|
||||
function getDisplayPartsFromComment(comment: readonly (JSDocText | JSDocLink)[], checker: TypeChecker | undefined): SymbolDisplayPart[] {
|
||||
return flatMap(
|
||||
comment,
|
||||
node => node.kind === SyntaxKind.JSDocText ? [textPart(node.text)] : buildLinkParts(node, checker)
|
||||
) as SymbolDisplayPart[];
|
||||
}
|
||||
|
||||
function getCommentDisplayParts(tag: JSDocTag, checker?: TypeChecker): SymbolDisplayPart[] | undefined {
|
||||
const { comment } = tag;
|
||||
switch (tag.kind) {
|
||||
case SyntaxKind.JSDocImplementsTag:
|
||||
@@ -131,7 +145,7 @@ namespace ts.JsDoc {
|
||||
case SyntaxKind.JSDocAugmentsTag:
|
||||
return withNode((tag as JSDocAugmentsTag).class);
|
||||
case SyntaxKind.JSDocTemplateTag:
|
||||
return withList((tag as JSDocTemplateTag).typeParameters);
|
||||
return addComment((tag as JSDocTemplateTag).typeParameters.map(tp => tp.getText()).join(", "));
|
||||
case SyntaxKind.JSDocTypeTag:
|
||||
return withNode((tag as JSDocTypeTag).typeExpression);
|
||||
case SyntaxKind.JSDocTypedefTag:
|
||||
@@ -140,21 +154,17 @@ namespace ts.JsDoc {
|
||||
case SyntaxKind.JSDocParameterTag:
|
||||
case SyntaxKind.JSDocSeeTag:
|
||||
const { name } = tag as JSDocTypedefTag | JSDocPropertyTag | JSDocParameterTag | JSDocSeeTag;
|
||||
return name ? withNode(name) : comment;
|
||||
return name ? withNode(name) : comment && getDisplayPartsFromComment(comment, checker);
|
||||
default:
|
||||
return comment;
|
||||
return comment && getDisplayPartsFromComment(comment, checker);
|
||||
}
|
||||
|
||||
function withNode(node: Node) {
|
||||
return addComment(node.getText());
|
||||
}
|
||||
|
||||
function withList(list: NodeArray<Node>): string {
|
||||
return addComment(list.map(x => x.getText()).join(", "));
|
||||
}
|
||||
|
||||
function addComment(s: string) {
|
||||
return comment === undefined ? s : `${s} ${comment}`;
|
||||
return comment ? [textPart(s), spacePart(), ...getDisplayPartsFromComment(comment, checker)] : [textPart(s)];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -292,14 +292,11 @@ namespace ts {
|
||||
// Undefined is used to indicate the value has not been computed. If, after computing, the
|
||||
// symbol has no doc comment, then the empty array will be returned.
|
||||
documentationComment?: SymbolDisplayPart[];
|
||||
tags?: JSDocTagInfo[]; // same
|
||||
|
||||
contextualGetAccessorDocumentationComment?: SymbolDisplayPart[];
|
||||
contextualSetAccessorDocumentationComment?: SymbolDisplayPart[];
|
||||
|
||||
// Undefined is used to indicate the value has not been computed. If, after computing, the
|
||||
// symbol has no JSDoc tags, then the empty array will be returned.
|
||||
tags?: JSDocTagInfo[];
|
||||
|
||||
constructor(flags: SymbolFlags, name: __String) {
|
||||
this.flags = flags;
|
||||
this.escapedName = name;
|
||||
@@ -359,9 +356,9 @@ namespace ts {
|
||||
}
|
||||
}
|
||||
|
||||
getJsDocTags(): JSDocTagInfo[] {
|
||||
getJsDocTags(checker?: TypeChecker): JSDocTagInfo[] {
|
||||
if (this.tags === undefined) {
|
||||
this.tags = JsDoc.getJsDocTagsFromDeclarations(this.declarations);
|
||||
this.tags = JsDoc.getJsDocTagsFromDeclarations(this.declarations, checker);
|
||||
}
|
||||
|
||||
return this.tags;
|
||||
@@ -521,10 +518,7 @@ namespace ts {
|
||||
// Undefined is used to indicate the value has not been computed. If, after computing, the
|
||||
// symbol has no doc comment, then the empty array will be returned.
|
||||
documentationComment?: SymbolDisplayPart[];
|
||||
|
||||
// Undefined is used to indicate the value has not been computed. If, after computing, the
|
||||
// symbol has no doc comment, then the empty array will be returned.
|
||||
jsDocTags?: JSDocTagInfo[];
|
||||
jsDocTags?: JSDocTagInfo[]; // same
|
||||
|
||||
constructor(checker: TypeChecker, flags: SignatureFlags) {
|
||||
this.checker = checker;
|
||||
@@ -566,7 +560,7 @@ namespace ts {
|
||||
}
|
||||
|
||||
function getJsDocTagsOfSignature(declaration: Declaration, checker: TypeChecker): JSDocTagInfo[] {
|
||||
let tags = JsDoc.getJsDocTagsFromDeclarations([declaration]);
|
||||
let tags = JsDoc.getJsDocTagsFromDeclarations([declaration], checker);
|
||||
if (tags.length === 0 || hasJSDocInheritDocTag(declaration)) {
|
||||
const inheritedTags = findBaseOfDeclaration(checker, declaration, symbol => symbol.declarations?.length === 1 ? symbol.getJsDocTags() : undefined);
|
||||
if (inheritedTags) {
|
||||
@@ -579,7 +573,7 @@ namespace ts {
|
||||
function getDocumentationComment(declarations: readonly Declaration[] | undefined, checker: TypeChecker | undefined): SymbolDisplayPart[] {
|
||||
if (!declarations) return emptyArray;
|
||||
|
||||
let doc = JsDoc.getJsDocCommentsFromDeclarations(declarations);
|
||||
let doc = JsDoc.getJsDocCommentsFromDeclarations(declarations, checker);
|
||||
if (checker && (doc.length === 0 || declarations.some(hasJSDocInheritDocTag))) {
|
||||
forEachUnique(declarations, declaration => {
|
||||
const inheritedDocs = findBaseOfDeclaration(checker, declaration, symbol => symbol.getDocumentationComment(checker));
|
||||
@@ -1598,7 +1592,7 @@ namespace ts {
|
||||
textSpan: createTextSpanFromNode(nodeForQuickInfo, sourceFile),
|
||||
displayParts: typeChecker.runWithCancellationToken(cancellationToken, typeChecker => typeToDisplayParts(typeChecker, type, getContainerNode(nodeForQuickInfo))),
|
||||
documentation: type.symbol ? type.symbol.getDocumentationComment(typeChecker) : undefined,
|
||||
tags: type.symbol ? type.symbol.getJsDocTags() : undefined
|
||||
tags: type.symbol ? type.symbol.getJsDocTags(typeChecker) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -574,7 +574,7 @@ namespace ts.SignatureHelp {
|
||||
const parameters = typeParameters.map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer));
|
||||
|
||||
const documentation = symbol.getDocumentationComment(checker);
|
||||
const tags = symbol.getJsDocTags();
|
||||
const tags = symbol.getJsDocTags(checker);
|
||||
const prefixDisplayParts = [...typeSymbolDisplay, punctuationPart(SyntaxKind.LessThanToken)];
|
||||
return { isVariadic: false, prefixDisplayParts, suffixDisplayParts: [punctuationPart(SyntaxKind.GreaterThanToken)], separatorDisplayParts, parameters, documentation, tags };
|
||||
}
|
||||
|
||||
@@ -441,7 +441,7 @@ namespace ts.SymbolDisplay {
|
||||
}
|
||||
else {
|
||||
documentationFromAlias = resolvedSymbol.getContextualDocumentationComment(resolvedNode, typeChecker);
|
||||
tagsFromAlias = resolvedSymbol.getJsDocTags();
|
||||
tagsFromAlias = resolvedSymbol.getJsDocTags(typeChecker);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -570,7 +570,7 @@ namespace ts.SymbolDisplay {
|
||||
}
|
||||
|
||||
documentation = rhsSymbol.getDocumentationComment(typeChecker);
|
||||
tags = rhsSymbol.getJsDocTags();
|
||||
tags = rhsSymbol.getJsDocTags(typeChecker);
|
||||
if (documentation.length > 0) {
|
||||
break;
|
||||
}
|
||||
@@ -579,7 +579,7 @@ namespace ts.SymbolDisplay {
|
||||
}
|
||||
|
||||
if (tags.length === 0 && !hasMultipleSignatures) {
|
||||
tags = symbol.getJsDocTags();
|
||||
tags = symbol.getJsDocTags(typeChecker);
|
||||
}
|
||||
|
||||
if (documentation.length === 0 && documentationFromAlias) {
|
||||
|
||||
+18
-2
@@ -43,7 +43,7 @@ namespace ts {
|
||||
getDocumentationComment(typeChecker: TypeChecker | undefined): SymbolDisplayPart[];
|
||||
/* @internal */
|
||||
getContextualDocumentationComment(context: Node | undefined, checker: TypeChecker | undefined): SymbolDisplayPart[]
|
||||
getJsDocTags(): JSDocTagInfo[];
|
||||
getJsDocTags(checker?: TypeChecker): JSDocTagInfo[];
|
||||
}
|
||||
|
||||
export interface Type {
|
||||
@@ -1032,6 +1032,9 @@ namespace ts {
|
||||
enumMemberName,
|
||||
functionName,
|
||||
regularExpressionLiteral,
|
||||
link,
|
||||
linkName,
|
||||
linkText,
|
||||
}
|
||||
|
||||
export interface SymbolDisplayPart {
|
||||
@@ -1039,9 +1042,13 @@ namespace ts {
|
||||
kind: string;
|
||||
}
|
||||
|
||||
export interface JSDocLinkDisplayPart extends SymbolDisplayPart {
|
||||
target: DocumentSpan;
|
||||
}
|
||||
|
||||
export interface JSDocTagInfo {
|
||||
name: string;
|
||||
text?: string;
|
||||
text?: SymbolDisplayPart[];
|
||||
}
|
||||
|
||||
export interface QuickInfo {
|
||||
@@ -1391,6 +1398,15 @@ namespace ts {
|
||||
|
||||
/** String literal */
|
||||
string = "string",
|
||||
|
||||
/** Jsdoc @link: in `{@link C link text}`, the before and after text "{@link " and "}" */
|
||||
link = "link",
|
||||
|
||||
/** Jsdoc @link: in `{@link C link text}`, the entity name "C" */
|
||||
linkName = "link name",
|
||||
|
||||
/** Jsdoc @link: in `{@link C link text}`, the link text "link text" */
|
||||
linkText = "link text",
|
||||
}
|
||||
|
||||
export const enum ScriptElementKindModifier {
|
||||
|
||||
@@ -105,7 +105,7 @@ namespace ts {
|
||||
else if (isDeclarationName(node)) {
|
||||
return getMeaningFromDeclaration(node.parent);
|
||||
}
|
||||
else if (isEntityName(node) && isJSDocNameReference(node.parent)) {
|
||||
else if (isEntityName(node) && (isJSDocNameReference(node.parent) || isJSDocLink(node.parent))) {
|
||||
return SemanticMeaning.All;
|
||||
}
|
||||
else if (isTypeReference(node)) {
|
||||
@@ -2183,6 +2183,44 @@ namespace ts {
|
||||
return displayPart(text, SymbolDisplayPartKind.text);
|
||||
}
|
||||
|
||||
export function linkTextPart(text: string) {
|
||||
return displayPart(text, SymbolDisplayPartKind.linkText);
|
||||
}
|
||||
|
||||
export function linkNamePart(name: EntityName, target: Declaration): JSDocLinkDisplayPart {
|
||||
return {
|
||||
text: getTextOfNode(name),
|
||||
kind: SymbolDisplayPartKind[SymbolDisplayPartKind.linkName],
|
||||
target: {
|
||||
fileName: getSourceFileOfNode(target).fileName,
|
||||
textSpan: createTextSpanFromNode(target),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function linkPart(text: string) {
|
||||
return displayPart(text, SymbolDisplayPartKind.link);
|
||||
}
|
||||
|
||||
export function buildLinkParts(link: JSDocLink, checker?: TypeChecker): SymbolDisplayPart[] {
|
||||
const parts = [linkPart("{@link ")];
|
||||
if (!link.name) {
|
||||
if (link.text) {parts.push(linkTextPart(link.text));}
|
||||
}
|
||||
else {
|
||||
const symbol = checker?.getSymbolAtLocation(link.name);
|
||||
if (symbol?.valueDeclaration) {
|
||||
parts.push(linkNamePart(link.name, symbol.valueDeclaration));
|
||||
if (link.text) {parts.push(linkTextPart(link.text));}
|
||||
}
|
||||
else {
|
||||
parts.push(linkTextPart(getTextOfNode(link.name) + link.text));
|
||||
}
|
||||
}
|
||||
parts.push(linkPart("}"));
|
||||
return parts;
|
||||
}
|
||||
|
||||
const carriageReturnLineFeed = "\r\n";
|
||||
/**
|
||||
* The default is CRLF.
|
||||
|
||||
Reference in New Issue
Block a user