Skip to content

Commit

Permalink
@link support, second try (#43312)
Browse files Browse the repository at this point in the history
* Revert "Revert "Editor support for link tag (#41877)" (#43302)"

This reverts commit 451d435.

* Fix parsing @link at end of comment

* Parse comments as string when no @link occurs

* fix lint
  • Loading branch information
sandersn authored Mar 22, 2021
1 parent 4622718 commit dcc27eb
Show file tree
Hide file tree
Showing 145 changed files with 20,102 additions and 1,575 deletions.
8 changes: 6 additions & 2 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5190,7 +5190,7 @@ namespace ts {
function preserveCommentsOn<T extends Node>(node: T) {
if (some(propertySymbol.declarations, d => d.kind === SyntaxKind.JSDocPropertyTag)) {
const d = propertySymbol.declarations?.find(d => d.kind === SyntaxKind.JSDocPropertyTag)! as JSDocPropertyTag;
const commentText = d.comment;
const commentText = getTextOfJSDocComment(d.comment);
if (commentText) {
setSyntheticLeadingComments(node, [{ kind: SyntaxKind.MultiLineCommentTrivia, text: "*\n * " + commentText.replace(/\n/g, "\n * ") + "\n ", pos: -1, end: -1, hasTrailingNewLine: true }]);
}
Expand Down Expand Up @@ -6705,7 +6705,7 @@ namespace ts {
const typeParams = getSymbolLinks(symbol).typeParameters;
const typeParamDecls = map(typeParams, p => typeParameterToDeclaration(p, context));
const jsdocAliasDecl = symbol.declarations?.find(isJSDocTypeAlias);
const commentText = jsdocAliasDecl ? jsdocAliasDecl.comment || jsdocAliasDecl.parent.comment : undefined;
const commentText = getTextOfJSDocComment(jsdocAliasDecl ? jsdocAliasDecl.comment || jsdocAliasDecl.parent.comment : undefined);
const oldFlags = context.flags;
context.flags |= NodeBuilderFlags.InTypeAlias;
const oldEnclosingDecl = context.enclosingDeclaration;
Expand Down Expand Up @@ -38673,6 +38673,10 @@ namespace ts {
const meaning = SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Value;
return resolveEntityName(<EntityName>name, meaning, /*ignoreErrors*/ false, /*dontResolveAlias*/ true, getHostSignatureFromJSDoc(name));
}
else if (isJSDocLink(name.parent)) {
const meaning = SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Value;
return resolveEntityName(<EntityName>name, meaning, /*ignoreErrors*/ true);
}

if (name.parent.kind === SyntaxKind.TypePredicate) {
return resolveEntityName(<Identifier>name, /*meaning*/ SymbolFlags.FunctionScopedVariable);
Expand Down
24 changes: 14 additions & 10 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3565,13 +3565,16 @@ namespace ts {
function emitJSDoc(node: JSDoc) {
write("/**");
if (node.comment) {
const lines = node.comment.split(/\r\n?|\n/g);
for (const line of lines) {
writeLine();
writeSpace();
writePunctuation("*");
writeSpace();
write(line);
const text = getTextOfJSDocComment(node.comment);
if (text) {
const lines = text.split(/\r\n?|\n/g);
for (const line of lines) {
writeLine();
writeSpace();
writePunctuation("*");
writeSpace();
write(line);
}
}
}
if (node.tags) {
Expand Down Expand Up @@ -3704,10 +3707,11 @@ namespace ts {
emit(tagName);
}

function emitJSDocComment(comment: string | undefined) {
if (comment) {
function emitJSDocComment(comment: string | NodeArray<JSDocText | JSDocLink> | undefined) {
const text = getTextOfJSDocComment(comment);
if (text) {
writeSpace();
write(comment);
write(text);
}
}

Expand Down
91 changes: 62 additions & 29 deletions src/compiler/factory/nodeFactory.ts

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/compiler/factory/nodeTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,10 @@ namespace ts {
return node.kind === SyntaxKind.JSDocNameReference;
}

export function isJSDocLink(node: Node): node is JSDocLink {
return node.kind === SyntaxKind.JSDocLink;
}

export function isJSDocAllType(node: Node): node is JSDocAllType {
return node.kind === SyntaxKind.JSDocAllType;
}
Expand Down
201 changes: 138 additions & 63 deletions src/compiler/parser.ts

Large diffs are not rendered by default.

105 changes: 61 additions & 44 deletions src/compiler/types.ts

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion src/compiler/utilitiesPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,13 @@ namespace ts {
return getJSDocTags(node).filter(doc => doc.kind === kind);
}

/** Gets the text of a jsdoc comment, flattening links to their text. */
export function getTextOfJSDocComment(comment?: string | NodeArray<JSDocText | JSDocLink>) {
return typeof comment === "string" ? comment
: comment?.map(c =>
c.kind === SyntaxKind.JSDocText ? c.text : `{@link ${c.name ? entityNameToString(c.name) + " " : ""}${c.text}}`).join("");
}

/**
* Gets the effective type parameters. If the node was parsed in a
* JavaScript file, gets the type parameters from the `@template` tag from JSDoc.
Expand Down Expand Up @@ -1860,7 +1867,13 @@ namespace ts {

/** True if node is of a kind that may contain comment text. */
export function isJSDocCommentContainingNode(node: Node): boolean {
return node.kind === SyntaxKind.JSDocComment || node.kind === SyntaxKind.JSDocNamepathType || isJSDocTag(node) || isJSDocTypeLiteral(node) || isJSDocSignature(node);
return node.kind === SyntaxKind.JSDocComment
|| node.kind === SyntaxKind.JSDocNamepathType
|| node.kind === SyntaxKind.JSDocText
|| node.kind === SyntaxKind.JSDocLink
|| isJSDocTag(node)
|| isJSDocTypeLiteral(node)
|| isJSDocSignature(node);
}

// TODO: determine what this does before making it public.
Expand Down
4 changes: 2 additions & 2 deletions src/deprecatedCompat/deprecations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1229,7 +1229,7 @@ namespace ts {

/** @deprecated Use `factory.createJSDocParameterTag` or the factory supplied by your transformation context instead. */
export const createJSDocParamTag = Debug.deprecate(function createJSDocParamTag(name: EntityName, isBracketed: boolean, typeExpression?: JSDocTypeExpression, comment?: string): JSDocParameterTag {
return factory.createJSDocParameterTag(/*tagName*/ undefined, name, isBracketed, typeExpression, /*isNameFirst*/ false, comment);
return factory.createJSDocParameterTag(/*tagName*/ undefined, name, isBracketed, typeExpression, /*isNameFirst*/ false, comment ? factory.createNodeArray([factory.createJSDocText(comment)]) : undefined);
}, factoryDeprecation);

/** @deprecated Use `factory.createComma` or the factory supplied by your transformation context instead. */
Expand Down Expand Up @@ -1374,4 +1374,4 @@ namespace ts {
});

// #endregion Renamed node Tests
}
}
16 changes: 12 additions & 4 deletions src/harness/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ namespace ts.server {
kindModifiers: body.kindModifiers,
textSpan: this.decodeSpan(body, fileName),
displayParts: [{ kind: "text", text: body.displayString }],
documentation: [{ kind: "text", text: body.documentation }],
tags: body.tags
documentation: typeof body.documentation === "string" ? [{ kind: "text", text: body.documentation }] : body.documentation,
tags: this.decodeLinkDisplayParts(body.tags)
};
}

Expand Down Expand Up @@ -536,6 +536,13 @@ namespace ts.server {
this.lineOffsetToPosition(fileName, span.end, lineMap));
}

private decodeLinkDisplayParts(tags: (protocol.JSDocTagInfo | JSDocTagInfo)[]): JSDocTagInfo[] {
return tags.map(tag => typeof tag.text === "string" ? {
...tag,
text: [textPart(tag.text)]
} : (tag as JSDocTagInfo));
}

getNameOrDottedNameSpan(_fileName: string, _startPos: number, _endPos: number): TextSpan {
return notImplemented();
}
Expand All @@ -554,9 +561,10 @@ namespace ts.server {
return undefined;
}

const { items, applicableSpan: encodedApplicableSpan, selectedItemIndex, argumentIndex, argumentCount } = response.body;
const { items: encodedItems, applicableSpan: encodedApplicableSpan, selectedItemIndex, argumentIndex, argumentCount } = response.body;

const applicableSpan = this.decodeSpan(encodedApplicableSpan, fileName);
const applicableSpan = encodedApplicableSpan as unknown as TextSpan;
const items = (encodedItems as (SignatureHelpItem | protocol.SignatureHelpItem)[]).map(item => ({ ...item, tags: this.decodeLinkDisplayParts(item.tags) }));

return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount };
}
Expand Down
2 changes: 1 addition & 1 deletion src/harness/fourslashImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1583,7 +1583,7 @@ namespace FourSlash {
assert.equal(actualTags.length, (options.tags || ts.emptyArray).length, this.assertionMessageAtLastKnownMarker("signature help tags"));
ts.zipWith((options.tags || ts.emptyArray), actualTags, (expectedTag, actualTag) => {
assert.equal(actualTag.name, expectedTag.name);
assert.equal(actualTag.text, expectedTag.text, this.assertionMessageAtLastKnownMarker("signature help tag " + actualTag.name));
assert.deepEqual(actualTag.text, expectedTag.text, this.assertionMessageAtLastKnownMarker("signature help tag " + actualTag.name));
});

const allKeys: readonly (keyof FourSlashInterface.VerifySignatureHelpOptions)[] = [
Expand Down
21 changes: 20 additions & 1 deletion src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,16 @@ namespace ts.server.protocol {
file: string;
}

export interface JSDocTagInfo {
/** Name of the JSDoc tag */
name: string;
/**
* Comment text after the JSDoc tag -- the text after the tag name until the next tag or end of comment
* Display parts when UserPreferences.displayPartsForJSDoc is true, flattened to string otherwise.
*/
text?: string | SymbolDisplayPart[];
}

export interface TextSpanWithContext extends TextSpan {
contextStart?: Location;
contextEnd?: Location;
Expand Down Expand Up @@ -1951,6 +1961,7 @@ namespace ts.server.protocol {
*/
export interface QuickInfoRequest extends FileLocationRequest {
command: CommandTypes.Quickinfo;
arguments: FileLocationRequestArgs;
}

/**
Expand Down Expand Up @@ -1984,8 +1995,9 @@ namespace ts.server.protocol {

/**
* Documentation associated with symbol.
* Display parts when UserPreferences.displayPartsForJSDoc is true, flattened to string otherwise.
*/
documentation: string;
documentation: string | SymbolDisplayPart[];

/**
* JSDoc tags associated with symbol.
Expand Down Expand Up @@ -2208,6 +2220,12 @@ namespace ts.server.protocol {
kind: string;
}

/** A part of a symbol description that links from a jsdoc @link tag to a declaration */
export interface JSDocLinkDisplayPart extends SymbolDisplayPart {
/** The location of the declaration that the @link tag links to. */
target: FileSpan;
}

/**
* An item found in a completion response.
*/
Expand Down Expand Up @@ -3301,6 +3319,7 @@ namespace ts.server.protocol {
readonly allowRenameOfImportPath?: boolean;
readonly includePackageJsonAutoImports?: "auto" | "on" | "off";

readonly displayPartsForJSDoc?: boolean;
readonly generateReturnInDocTemplate?: boolean;
}

Expand Down
77 changes: 56 additions & 21 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1274,6 +1274,32 @@ namespace ts.server {
result;
}

private mapJSDocTagInfo(tags: JSDocTagInfo[] | undefined, project: Project, richResponse: boolean): protocol.JSDocTagInfo[] {
return tags ? tags.map(tag => ({
...tag,
text: richResponse ? this.mapDisplayParts(tag.text, project) : tag.text?.map(part => part.text).join("")
})) : [];
}

private mapDisplayParts(parts: SymbolDisplayPart[] | undefined, project: Project): protocol.SymbolDisplayPart[] {
if (!parts) {
return [];
}
return parts.map(part => part.kind !== "linkName" ? part : {
...part,
target: this.toFileSpan((part as JSDocLinkDisplayPart).target.fileName, (part as JSDocLinkDisplayPart).target.textSpan, project),
});
}

private mapSignatureHelpItems(items: SignatureHelpItem[], project: Project, richResponse: boolean): protocol.SignatureHelpItem[] {
return items.map(item => ({
...item,
documentation: this.mapDisplayParts(item.documentation, project),
parameters: item.parameters.map(p => ({ ...p, documentation: this.mapDisplayParts(p.documentation, project) })),
tags: this.mapJSDocTagInfo(item.tags, project, richResponse),
}));
}

private mapDefinitionInfo(definitions: readonly DefinitionInfo[], project: Project): readonly protocol.FileSpanWithContext[] {
return definitions.map(def => this.toFileSpanWithContext(def.fileName, def.textSpan, def.contextSpan, project));
}
Expand Down Expand Up @@ -1685,22 +1711,24 @@ namespace ts.server {
return undefined;
}

const useDisplayParts = !!this.getPreferences(file).displayPartsForJSDoc;
if (simplifiedResult) {
const displayString = displayPartsToString(quickInfo.displayParts);
const docString = displayPartsToString(quickInfo.documentation);

return {
kind: quickInfo.kind,
kindModifiers: quickInfo.kindModifiers,
start: scriptInfo.positionToLineOffset(quickInfo.textSpan.start),
end: scriptInfo.positionToLineOffset(textSpanEnd(quickInfo.textSpan)),
displayString,
documentation: docString,
tags: quickInfo.tags || []
documentation: useDisplayParts ? this.mapDisplayParts(quickInfo.documentation, project) : displayPartsToString(quickInfo.documentation),
tags: this.mapJSDocTagInfo(quickInfo.tags, project, useDisplayParts),
};
}
else {
return quickInfo;
return useDisplayParts ? quickInfo : {
...quickInfo,
tags: this.mapJSDocTagInfo(quickInfo.tags, project, /*useDisplayParts*/ false) as JSDocTagInfo[]
};
}
}

Expand Down Expand Up @@ -1830,19 +1858,25 @@ namespace ts.server {
return res;
}

private getCompletionEntryDetails(args: protocol.CompletionDetailsRequestArgs, simplifiedResult: boolean): readonly protocol.CompletionEntryDetails[] | readonly CompletionEntryDetails[] {
private getCompletionEntryDetails(args: protocol.CompletionDetailsRequestArgs, fullResult: boolean): readonly protocol.CompletionEntryDetails[] | readonly CompletionEntryDetails[] {
const { file, project } = this.getFileAndProject(args);
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!;
const position = this.getPosition(args, scriptInfo);
const formattingOptions = project.projectService.getFormatCodeOptions(file);
const useDisplayParts = !!this.getPreferences(file).displayPartsForJSDoc;

const result = mapDefined(args.entryNames, entryName => {
const { name, source, data } = typeof entryName === "string" ? { name: entryName, source: undefined, data: undefined } : entryName;
return project.getLanguageService().getCompletionEntryDetails(file, position, name, formattingOptions, source, this.getPreferences(file), data ? cast(data, isCompletionEntryData) : undefined);
});
return simplifiedResult
? result.map(details => ({ ...details, codeActions: map(details.codeActions, action => this.mapCodeAction(action)) }))
: result;
return fullResult
? (useDisplayParts ? result : result.map(details => ({ ...details, tags: this.mapJSDocTagInfo(details.tags, project, /*richResponse*/ false) as JSDocTagInfo[] })))
: result.map(details => ({
...details,
codeActions: map(details.codeActions, action => this.mapCodeAction(action)),
documentation: this.mapDisplayParts(details.documentation, project),
tags: this.mapJSDocTagInfo(details.tags, project, useDisplayParts),
}));
}

private getCompileOnSaveAffectedFileList(args: protocol.FileRequestArgs): readonly protocol.CompileOnSaveAffectedFileListSingleProject[] {
Expand Down Expand Up @@ -1902,26 +1936,27 @@ namespace ts.server {
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!;
const position = this.getPosition(args, scriptInfo);
const helpItems = project.getLanguageService().getSignatureHelpItems(file, position, args);
if (!helpItems) {
return undefined;
}

if (simplifiedResult) {
const useDisplayParts = !!this.getPreferences(file).displayPartsForJSDoc;
if (helpItems && simplifiedResult) {
const span = helpItems.applicableSpan;
return {
items: helpItems.items,
...helpItems,
applicableSpan: {
start: scriptInfo.positionToLineOffset(span.start),
end: scriptInfo.positionToLineOffset(span.start + span.length)
},
selectedItemIndex: helpItems.selectedItemIndex,
argumentIndex: helpItems.argumentIndex,
argumentCount: helpItems.argumentCount,
items: this.mapSignatureHelpItems(helpItems.items, project, useDisplayParts),
};
}
else {
else if (useDisplayParts || !helpItems) {
return helpItems;
}
else {
return {
...helpItems,
items: helpItems.items.map(item => ({ ...item, tags: this.mapJSDocTagInfo(item.tags, project, /*richResponse*/ false) as JSDocTagInfo[] }))
};
}
}

private toPendingErrorCheck(uncheckedFileName: string): PendingErrorCheck | undefined {
Expand Down Expand Up @@ -2700,10 +2735,10 @@ namespace ts.server {
return this.requiredResponse(this.getCompletions(request.arguments, CommandNames.CompletionsFull));
},
[CommandNames.CompletionDetails]: (request: protocol.CompletionDetailsRequest) => {
return this.requiredResponse(this.getCompletionEntryDetails(request.arguments, /*simplifiedResult*/ true));
return this.requiredResponse(this.getCompletionEntryDetails(request.arguments, /*fullResult*/ false));
},
[CommandNames.CompletionDetailsFull]: (request: protocol.CompletionDetailsRequest) => {
return this.requiredResponse(this.getCompletionEntryDetails(request.arguments, /*simplifiedResult*/ false));
return this.requiredResponse(this.getCompletionEntryDetails(request.arguments, /*fullResult*/ true));
},
[CommandNames.CompileOnSaveAffectedFileList]: (request: protocol.CompileOnSaveAffectedFileListRequest) => {
return this.requiredResponse(this.getCompileOnSaveAffectedFileList(request.arguments));
Expand Down
Loading

0 comments on commit dcc27eb

Please sign in to comment.