Skip to content

Commit

Permalink
feat: add support for @link references in JSDoc (#612)
Browse files Browse the repository at this point in the history
  • Loading branch information
rchl committed Oct 17, 2022
1 parent a03eab5 commit 3722b51
Show file tree
Hide file tree
Showing 8 changed files with 554 additions and 120 deletions.
43 changes: 33 additions & 10 deletions src/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import * as lsp from 'vscode-languageserver';
import type tsp from 'typescript/lib/protocol.js';
import { LspDocument } from './document.js';
import { CommandTypes, KindModifiers, ScriptElementKind } from './tsp-command-types.js';
import { toTextEdit, asPlainText, asDocumentation, normalizePath } from './protocol-translation.js';
import { toTextEdit, normalizePath } from './protocol-translation.js';
import { Commands } from './commands.js';
import { TspClient } from './tsp-client.js';
import { DisplayPartKind, SupportedFeatures } from './ts-protocol.js';
import * as Previewer from './utils/previewer.js';
import { IFilePathToResourceConverter } from './utils/previewer.js';
import SnippetString from './utils/SnippetString.js';
import { Range, Position } from './utils/typeConverters.js';
import type { WorkspaceConfigurationCompletionOptions } from './configuration-manager.js';
Expand All @@ -22,7 +24,15 @@ interface ParameterListParts {
readonly hasOptionalParameters: boolean;
}

export function asCompletionItem(entry: tsp.CompletionEntry, optionalReplacementSpan: tsp.TextSpan | undefined, file: string, position: lsp.Position, document: LspDocument, options: WorkspaceConfigurationCompletionOptions, features: SupportedFeatures): lsp.CompletionItem | null {
export function asCompletionItem(
entry: tsp.CompletionEntry,
optionalReplacementSpan: tsp.TextSpan | undefined,
file: string, position: lsp.Position,
document: LspDocument,
filePathConverter: IFilePathToResourceConverter,
options: WorkspaceConfigurationCompletionOptions,
features: SupportedFeatures,
): lsp.CompletionItem | null {
const item: lsp.CompletionItem = {
label: entry.name,
...features.completionLabelDetails ? { labelDetails: entry.labelDetails } : {},
Expand Down Expand Up @@ -59,7 +69,7 @@ export function asCompletionItem(entry: tsp.CompletionEntry, optionalReplacement
item.insertTextFormat = lsp.InsertTextFormat.Snippet;
}
if (sourceDisplay) {
item.detail = asPlainText(sourceDisplay);
item.detail = Previewer.plainWithLinks(sourceDisplay, filePathConverter);
}

let { insertText } = entry;
Expand Down Expand Up @@ -108,7 +118,11 @@ export function asCompletionItem(entry: tsp.CompletionEntry, optionalReplacement
}

function getRangeFromReplacementSpan(
replacementSpan: tsp.TextSpan | undefined, optionalReplacementSpan: tsp.TextSpan | undefined, position: lsp.Position, document: LspDocument, features: SupportedFeatures,
replacementSpan: tsp.TextSpan | undefined,
optionalReplacementSpan: tsp.TextSpan | undefined,
position: lsp.Position,
document: LspDocument,
features: SupportedFeatures,
): { insert?: lsp.Range; replace: lsp.Range; } | undefined {
if (replacementSpan) {
// If TS provides an explicit replacement span with an entry, we should use it and not provide an insert.
Expand Down Expand Up @@ -208,10 +222,16 @@ function asCommitCharacters(kind: ScriptElementKind): string[] | undefined {
}

export async function asResolvedCompletionItem(
item: lsp.CompletionItem, details: tsp.CompletionEntryDetails, client: TspClient, options: WorkspaceConfigurationCompletionOptions, features: SupportedFeatures,
item: lsp.CompletionItem,
details: tsp.CompletionEntryDetails,
client: TspClient,
filePathConverter: IFilePathToResourceConverter,
options: WorkspaceConfigurationCompletionOptions,
features: SupportedFeatures,
): Promise<lsp.CompletionItem> {
item.detail = asDetail(details);
item.documentation = asDocumentation(details);
item.detail = asDetail(details, filePathConverter);
const { documentation, tags } = details;
item.documentation = Previewer.markdownDocumentation(documentation, tags, filePathConverter);
const filepath = normalizePath(item.data.file);
if (details.codeActions?.length) {
item.additionalTextEdits = asAdditionalTextEdits(details.codeActions, filepath);
Expand Down Expand Up @@ -393,13 +413,16 @@ function asCommand(codeActions: tsp.CodeAction[], filepath: string): lsp.Command
}
}

function asDetail({ displayParts, sourceDisplay, source: deprecatedSource }: tsp.CompletionEntryDetails): string | undefined {
function asDetail(
{ displayParts, sourceDisplay, source: deprecatedSource }: tsp.CompletionEntryDetails,
filePathConverter: IFilePathToResourceConverter,
): string | undefined {
const result: string[] = [];
const source = sourceDisplay || deprecatedSource;
if (source) {
result.push(`Auto import from '${asPlainText(source)}'`);
result.push(`Auto import from '${Previewer.plainWithLinks(source, filePathConverter)}'`);
}
const detail = asPlainText(displayParts);
const detail = Previewer.plainWithLinks(displayParts, filePathConverter);
if (detail) {
result.push(detail);
}
Expand Down
14 changes: 13 additions & 1 deletion src/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/

import vscodeUri from 'vscode-uri';
import * as lsp from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { IFilePathToResourceConverter } from './utils/previewer.js';

export class LspDocument implements TextDocument {
protected document: TextDocument;
Expand Down Expand Up @@ -81,7 +83,7 @@ export class LspDocument implements TextDocument {
}
}

export class LspDocuments {
export class LspDocuments implements IFilePathToResourceConverter {
private readonly _files: string[] = [];
private readonly documents = new Map<string, LspDocument>();

Expand Down Expand Up @@ -122,4 +124,14 @@ export class LspDocuments {
this._files.splice(this._files.indexOf(file), 1);
return document;
}

/* IFilePathToResourceConverter implementation */

public toResource(filepath: string): vscodeUri.URI {
const document = this.documents.get(filepath);
if (document) {
return vscodeUri.URI.parse(document.uri);
}
return vscodeUri.URI.file(filepath);
}
}
33 changes: 18 additions & 15 deletions src/hover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@

import * as lsp from 'vscode-languageserver';
import type tsp from 'typescript/lib/protocol.d.js';
import { asDocumentation, asPlainText } from './protocol-translation.js';
import * as Previewer from './utils/previewer.js';
import { IFilePathToResourceConverter } from './utils/previewer.js';

export function asSignatureHelp(info: tsp.SignatureHelpItems, context?: lsp.SignatureHelpContext): lsp.SignatureHelp {
const signatures = info.items.map(asSignatureInformation);
export function asSignatureHelp(
info: tsp.SignatureHelpItems,
context: lsp.SignatureHelpContext | undefined,
filePathConverter: IFilePathToResourceConverter,
): lsp.SignatureHelp {
const signatures = info.items.map(item => asSignatureInformation(item, filePathConverter));
return {
activeSignature: getActiveSignature(info, signatures, context),
activeParameter: getActiveParameter(info),
Expand Down Expand Up @@ -45,25 +50,23 @@ function getActiveParameter(info: tsp.SignatureHelpItems): number {
return info.argumentIndex;
}

function asSignatureInformation(item: tsp.SignatureHelpItem): lsp.SignatureInformation {
const parameters = item.parameters.map(asParameterInformation);
function asSignatureInformation(item: tsp.SignatureHelpItem, filePathConverter: IFilePathToResourceConverter): lsp.SignatureInformation {
const parameters = item.parameters.map(parameter => asParameterInformation(parameter, filePathConverter));
const signature: lsp.SignatureInformation = {
label: asPlainText(item.prefixDisplayParts),
documentation: asDocumentation({
documentation: item.documentation,
tags: item.tags.filter(x => x.name !== 'param'),
}),
label: Previewer.plainWithLinks(item.prefixDisplayParts, filePathConverter),
documentation: Previewer.markdownDocumentation(item.documentation, item.tags.filter(x => x.name !== 'param'), filePathConverter),
parameters,
};
signature.label += parameters.map(parameter => parameter.label).join(asPlainText(item.separatorDisplayParts));
signature.label += asPlainText(item.suffixDisplayParts);
signature.label += parameters.map(parameter => parameter.label).join(Previewer.plainWithLinks(item.separatorDisplayParts, filePathConverter));
signature.label += Previewer.plainWithLinks(item.suffixDisplayParts, filePathConverter);
return signature;
}

function asParameterInformation(parameter: tsp.SignatureHelpParameter): lsp.ParameterInformation {
function asParameterInformation(parameter: tsp.SignatureHelpParameter, filePathConverter: IFilePathToResourceConverter): lsp.ParameterInformation {
const { displayParts, documentation } = parameter;
return {
label: asPlainText(parameter.displayParts),
documentation: asDocumentation(parameter),
label: Previewer.plainWithLinks(displayParts, filePathConverter),
documentation: Previewer.markdownDocumentation(documentation, undefined, filePathConverter),
};
}

Expand Down
26 changes: 13 additions & 13 deletions src/lsp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { CommandTypes, EventTypes } from './tsp-command-types.js';
import { Logger, LogLevel, PrefixingLogger } from './utils/logger.js';
import { TspClient } from './tsp-client.js';
import { DiagnosticEventQueue } from './diagnostic-queue.js';
import { toDocumentHighlight, asTagsDocumentation, uriToPath, toSymbolKind, toLocation, pathToUri, toTextEdit, asPlainText, normalizePath } from './protocol-translation.js';
import { toDocumentHighlight, uriToPath, toSymbolKind, toLocation, pathToUri, toTextEdit, normalizePath } from './protocol-translation.js';
import { LspDocuments, LspDocument } from './document.js';
import { asCompletionItem, asResolvedCompletionItem, getCompletionTriggerCharacter } from './completion.js';
import { asSignatureHelp, toTsTriggerReason } from './hover.js';
Expand All @@ -36,6 +36,8 @@ import { SourceDefinitionCommand } from './features/source-definition.js';
import { LogDirectoryProvider } from './tsServer/logDirectoryProvider.js';
import { Trace } from './tsServer/tracer.js';
import { TypeScriptVersion, TypeScriptVersionProvider } from './tsServer/versionProvider.js';
import { MarkdownString } from './utils/MarkdownString.js';
import * as Previewer from './utils/previewer.js';
import { getInferredProjectCompilerOptions } from './utils/tsconfig.js';
import { Position, Range } from './utils/typeConverters.js';
import { CodeActionKind } from './utils/types.js';
Expand Down Expand Up @@ -609,7 +611,7 @@ export class LspServer {
if (entry.kind === 'warning') {
continue;
}
const completion = asCompletionItem(entry, optionalReplacementSpan, file, params.position, document, completionOptions, this.features);
const completion = asCompletionItem(entry, optionalReplacementSpan, file, params.position, document, this.documents, completionOptions, this.features);
if (!completion) {
continue;
}
Expand All @@ -634,7 +636,7 @@ export class LspServer {
if (!details) {
return item;
}
return asResolvedCompletionItem(item, details, this.tspClient, this.configurationManager.workspaceConfiguration.completions || {}, this.features);
return asResolvedCompletionItem(item, details, this.tspClient, this.documents, this.configurationManager.workspaceConfiguration.completions || {}, this.features);
}

async hover(params: lsp.TextDocumentPositionParams): Promise<lsp.Hover> {
Expand All @@ -648,17 +650,15 @@ export class LspServer {
if (!result || !result.body) {
return { contents: [] };
}
const range = Range.fromTextSpan(result.body);
const contents: lsp.MarkedString[] = [];
if (result.body.displayString) {
contents.push({ language: 'typescript', value: result.body.displayString });
const contents = new MarkdownString();
const { displayString, documentation, tags } = result.body;
if (displayString) {
contents.appendCodeblock('typescript', displayString);
}
const tags = asTagsDocumentation(result.body.tags);
const documentation = asPlainText(result.body.documentation);
contents.push(documentation + (tags ? '\n\n' + tags : ''));
Previewer.addMarkdownDocumentation(contents, documentation, tags, this.documents);
return {
contents,
range,
contents: contents.toMarkupContent(),
range: Range.fromTextSpan(result.body),
};
}
protected async getQuickInfo(file: string, position: lsp.Position): Promise<tsp.QuickInfoResponse | undefined> {
Expand Down Expand Up @@ -791,7 +791,7 @@ export class LspServer {
if (!response || !response.body) {
return undefined;
}
return asSignatureHelp(response.body, params.context);
return asSignatureHelp(response.body, params.context, this.documents);
}
protected async getSignatureHelp(file: string, params: lsp.SignatureHelpParams): Promise<tsp.SignatureHelpResponse | undefined> {
try {
Expand Down
81 changes: 0 additions & 81 deletions src/protocol-translation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,84 +209,3 @@ function toDocumentHighlightKind(kind: tsp.HighlightSpanKind): lsp.DocumentHighl
default: return lsp.DocumentHighlightKind.Text;
}
}

export function asDocumentation(data: {
documentation?: tsp.SymbolDisplayPart[];
tags?: tsp.JSDocTagInfo[];
}): lsp.MarkupContent | undefined {
let value = '';
if (data.documentation) {
value += asPlainText(data.documentation);
}
if (data.tags) {
const tagsDocumentation = asTagsDocumentation(data.tags);
if (tagsDocumentation) {
value += '\n\n' + tagsDocumentation;
}
}
return value.length ? {
kind: lsp.MarkupKind.Markdown,
value,
} : undefined;
}

export function asTagsDocumentation(tags: tsp.JSDocTagInfo[]): string {
return tags.map(asTagDocumentation).join(' \n\n');
}

export function asTagDocumentation(tag: tsp.JSDocTagInfo): string {
switch (tag.name) {
case 'param': {
if (!tag.text) {
break;
}
const text = asPlainText(tag.text);
const body = text.split(/^([\w.]+)\s*-?\s*/);
if (body && body.length === 3) {
const param = body[1];
const doc = body[2];
const label = `*@${tag.name}* \`${param}\``;
if (!doc) {
return label;
}
return label + (doc.match(/\r\n|\n/g) ? ' \n' + doc : ` — ${doc}`);
}
break;
}
}

// Generic tag
const label = `*@${tag.name}*`;
const text = asTagBodyText(tag);
if (!text) {
return label;
}
return label + (text.match(/\r\n|\n/g) ? ' \n' + text : ` — ${text}`);
}

export function asTagBodyText(tag: tsp.JSDocTagInfo): string | undefined {
if (!tag.text) {
return undefined;
}

const text = asPlainText(tag.text);

switch (tag.name) {
case 'example':
case 'default':
// Convert to markdown code block if it not already one
if (text.match(/^\s*[~`]{3}/g)) {
return text;
}
return '```\n' + text + '\n```';
}

return text;
}

export function asPlainText(parts: string | tsp.SymbolDisplayPart[]): string {
if (typeof parts === 'string') {
return parts;
}
return parts.map(part => part.text).join('');
}
56 changes: 56 additions & 0 deletions src/utils/MarkdownString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*
* Copyright (C) 2022 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/

import type * as lsp from 'vscode-languageserver/node.js';

export const enum MarkdownStringTextNewlineStyle {
Paragraph = 0,
Break = 1,
}

export class MarkdownString {
constructor(public value = '') {}

appendText(value: string, newlineStyle: MarkdownStringTextNewlineStyle = MarkdownStringTextNewlineStyle.Paragraph): MarkdownString {
this.value += escapeMarkdownSyntaxTokens(value)
.replace(/([ \t]+)/g, (_match, g1) => '&nbsp;'.repeat(g1.length))
.replace(/>/gm, '\\>')
.replace(/\n/g, newlineStyle === MarkdownStringTextNewlineStyle.Break ? '\\\n' : '\n\n');

return this;
}

appendMarkdown(value: string): MarkdownString {
this.value += value;
return this;
}

appendCodeblock(langId: string, code: string): MarkdownString {
this.value += '\n```';
this.value += langId;
this.value += '\n';
this.value += code;
this.value += '\n```\n';
return this;
}

toMarkupContent(): lsp.MarkupContent {
return {
kind: 'markdown',
value: this.value,
};
}
}

export function escapeMarkdownSyntaxTokens(text: string): string {
// escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash
return text.replace(/[\\`*_{}[\]()#+\-!]/g, '\\$&');
}

0 comments on commit 3722b51

Please sign in to comment.