Skip to content

Commit

Permalink
Add support for JSDoc see tags
Browse files Browse the repository at this point in the history
Fixes microsoft#119357
Fixes microsoft#89966

In order to implement this, I also added API that lets markdown strings allow just a subset of command uris. This is required because we have to use a command to implement the links, but do not want to allow just any command to be run
  • Loading branch information
mjbvz committed Mar 19, 2021
1 parent 5c0c637 commit c4ced96
Show file tree
Hide file tree
Showing 15 changed files with 169 additions and 42 deletions.
2 changes: 1 addition & 1 deletion build/monaco/monaco.d.ts.recipe
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ declare namespace monaco {
#include(vs/base/common/uri): URI, UriComponents
#include(vs/base/common/keyCodes): KeyCode
#include(vs/editor/common/standalone/standaloneBase): KeyMod
#include(vs/base/common/htmlContent): IMarkdownString
#include(vs/base/common/htmlContent): IMarkdownStringTrustSettings, IMarkdownString
#include(vs/base/browser/keyboardEvent): IKeyboardEvent
#include(vs/base/browser/mouseEvent): IMouseEvent
#include(vs/editor/common/editorCommon): IScrollEvent
Expand Down
2 changes: 2 additions & 0 deletions extensions/typescript-language-features/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { CommandManager } from './commandManager';
import { ConfigurePluginCommand } from './configurePlugin';
import { JavaScriptGoToProjectConfigCommand, TypeScriptGoToProjectConfigCommand } from './goToProjectConfiguration';
import { LearnMoreAboutRefactoringsCommand } from './learnMoreAboutRefactorings';
import { OpenJsDocLinkCommand } from './openJsDocLink';
import { OpenTsServerLogCommand } from './openTsServerLog';
import { ReloadJavaScriptProjectsCommand, ReloadTypeScriptProjectsCommand } from './reloadProject';
import { RestartTsServerCommand } from './restartTsServer';
Expand All @@ -25,6 +26,7 @@ export function registerBaseCommands(
commandManager.register(new ReloadTypeScriptProjectsCommand(lazyClientHost));
commandManager.register(new ReloadJavaScriptProjectsCommand(lazyClientHost));
commandManager.register(new SelectTypeScriptVersionCommand(lazyClientHost));
commandManager.register(new OpenJsDocLinkCommand(lazyClientHost));
commandManager.register(new OpenTsServerLogCommand(lazyClientHost));
commandManager.register(new RestartTsServerCommand(lazyClientHost));
commandManager.register(new TypeScriptGoToProjectConfigCommand(activeJsTsEditorTracker, lazyClientHost));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import TypeScriptServiceClientHost from '../typeScriptServiceClientHost';
import { Lazy } from '../utils/lazy';
import { Command } from './commandManager';


export class OpenJsDocLinkCommand implements Command {
public static readonly id = '_typescript.openJsDocLink';
public readonly id = OpenJsDocLinkCommand.id;

public static createCommandUri(fileSpan: Proto.FileSpan) {
return `command:${OpenJsDocLinkCommand.id}?${encodeURIComponent(JSON.stringify({ span: fileSpan }))}`;
}

public constructor(
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>
) { }

public async execute(args: { span: Proto.FileSpan }) {
if (!this.lazyClientHost.hasValue) {
return;
}

const uri = this.lazyClientHost.value.serviceClient.toResource(args.span.file);
const doc = await vscode.workspace.openTextDocument(uri);
const editor = await vscode.window.showTextDocument(doc);

const pos = new vscode.Position(args.span.start.line - 1, args.span.start.offset - 1);
const selection = new vscode.Selection(pos, pos);
editor.revealRange(selection, vscode.TextEditorRevealType.InCenterIfOutsideViewport);
editor.selection = selection;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ class MyCompletionItem extends vscode.CompletionItem {
const detail = response.body[0];

if (!this.detail && detail.displayParts.length) {
this.detail = Previewer.plain(detail.displayParts);
this.detail = Previewer.plainWithLinks(detail.displayParts);
}
this.documentation = this.getDocumentation(detail, this);

Expand Down Expand Up @@ -242,7 +242,7 @@ class MyCompletionItem extends vscode.CompletionItem {
): vscode.MarkdownString | undefined {
const documentation = new vscode.MarkdownString();
if (detail.source) {
const importPath = `'${Previewer.plain(detail.source)}'`;
const importPath = `'${Previewer.plainWithLinks(detail.source)}'`;
const autoImportLabel = localize('autoImportLabel', 'Auto import from {0}', importPath);
item.detail = `${autoImportLabel}\n${item.detail}`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export default class FileConfigurationManager extends Disposable {
includeAutomaticOptionalChainCompletions: config.get<boolean>('suggest.includeAutomaticOptionalChainCompletions', true),
provideRefactorNotApplicableReason: true,
generateReturnInDocTemplate: config.get<boolean>('suggest.jsdoc.generateReturns', true),
displayPartsForJSDoc: true,
};

return preferences;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import { conditionalRegistration, requireSomeCapability } from '../utils/depende
import { DocumentSelector } from '../utils/documentSelector';
import { markdownDocumentation } from '../utils/previewer';
import * as typeConverters from '../utils/typeConverters';
import FileConfigurationManager from './fileConfigurationManager';


class TypeScriptHoverProvider implements vscode.HoverProvider {

public constructor(
private readonly client: ITypeScriptServiceClient
private readonly client: ITypeScriptServiceClient,
private readonly fileConfigurationManager: FileConfigurationManager,
) { }

public async provideHover(
Expand All @@ -29,8 +31,13 @@ class TypeScriptHoverProvider implements vscode.HoverProvider {
return undefined;
}

const args = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
const response = await this.client.interruptGetErr(() => this.client.execute('quickinfo', args, token));
const response = await this.client.interruptGetErr(async () => {
await this.fileConfigurationManager.ensureConfigurationForDocument(document, token);

const args = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
return this.client.execute('quickinfo', args, token);
});

if (response.type !== 'response' || !response.body) {
return undefined;
}
Expand Down Expand Up @@ -69,12 +76,13 @@ class TypeScriptHoverProvider implements vscode.HoverProvider {

export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient
client: ITypeScriptServiceClient,
fileConfigurationManager: FileConfigurationManager,
): vscode.Disposable {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic),
], () => {
return vscode.languages.registerHoverProvider(selector.syntax,
new TypeScriptHoverProvider(client));
new TypeScriptHoverProvider(client, fileConfigurationManager));
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ class TypeScriptSignatureHelpProvider implements vscode.SignatureHelpProvider {

private convertSignature(item: Proto.SignatureHelpItem) {
const signature = new vscode.SignatureInformation(
Previewer.plain(item.prefixDisplayParts),
Previewer.plainWithLinks(item.prefixDisplayParts),
Previewer.markdownDocumentation(item.documentation, item.tags.filter(x => x.name !== 'param')));

let textIndex = signature.label.length;
const separatorLabel = Previewer.plain(item.separatorDisplayParts);
const separatorLabel = Previewer.plainWithLinks(item.separatorDisplayParts);
for (let i = 0; i < item.parameters.length; ++i) {
const parameter = item.parameters[i];
const label = Previewer.plain(parameter.displayParts);
const label = Previewer.plainWithLinks(parameter.displayParts);

signature.parameters.push(
new vscode.ParameterInformation(
Expand All @@ -95,7 +95,7 @@ class TypeScriptSignatureHelpProvider implements vscode.SignatureHelpProvider {
}
}

signature.label += Previewer.plain(item.suffixDisplayParts);
signature.label += Previewer.plainWithLinks(item.suffixDisplayParts);
return signature;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default class LanguageProvider extends Disposable {
import('./languageFeatures/fixAll').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.client.diagnosticsManager))),
import('./languageFeatures/folding').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/formatting').then(provider => this._register(provider.register(selector, this.description.id, this.client, this.fileConfigurationManager))),
import('./languageFeatures/hover').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/hover').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager))),
import('./languageFeatures/implementations').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/jsDocCompletions').then(provider => this._register(provider.register(selector, this.description.id, this.client, this.fileConfigurationManager))),
import('./languageFeatures/organizeImports').then(provider => this._register(provider.register(selector, this.client, this.commandManager, this.fileConfigurationManager, this.telemetryReporter))),
Expand Down
80 changes: 66 additions & 14 deletions extensions/typescript-language-features/src/utils/previewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { OpenJsDocLinkCommand } from '../commands/openJsDocLink';
import type * as Proto from '../protocol';

function replaceLinks(text: string): string {
Expand Down Expand Up @@ -37,29 +38,30 @@ function getTagBodyText(tag: Proto.JSDocTagInfo): string | undefined {
return '```\n' + text + '\n```';
}

const text = convertLinkTags(tag.text);
switch (tag.name) {
case 'example':
// check for caption tags, fix for #79704
const captionTagMatches = tag.text.match(/<caption>(.*?)<\/caption>\s*(\r\n|\n)/);
const captionTagMatches = text.match(/<caption>(.*?)<\/caption>\s*(\r\n|\n)/);
if (captionTagMatches && captionTagMatches.index === 0) {
return captionTagMatches[1] + '\n\n' + makeCodeblock(tag.text.substr(captionTagMatches[0].length));
return captionTagMatches[1] + '\n\n' + makeCodeblock(text.substr(captionTagMatches[0].length));
} else {
return makeCodeblock(tag.text);
return makeCodeblock(text);
}
case 'author':
// fix obsucated email address, #80898
const emailMatch = tag.text.match(/(.+)\s<([-.\w]+@[-.\w]+)>/);
const emailMatch = text.match(/(.+)\s<([-.\w]+@[-.\w]+)>/);

if (emailMatch === null) {
return tag.text;
return text;
} else {
return `${emailMatch[1]} ${emailMatch[2]}`;
}
case 'default':
return makeCodeblock(tag.text);
return makeCodeblock(text);
}

return processInlineTags(tag.text);
return processInlineTags(text);
}

function getTagDocumentation(tag: Proto.JSDocTagInfo): string | undefined {
Expand All @@ -68,7 +70,7 @@ function getTagDocumentation(tag: Proto.JSDocTagInfo): string | undefined {
case 'extends':
case 'param':
case 'template':
const body = (tag.text || '').split(/^(\S+)\s*-?\s*/);
const body = (convertLinkTags(tag.text)).split(/^(\S+)\s*-?\s*/);
if (body?.length === 3) {
const param = body[1];
const doc = body[2];
Expand All @@ -89,11 +91,58 @@ function getTagDocumentation(tag: Proto.JSDocTagInfo): string | undefined {
return label + (text.match(/\r\n|\n/g) ? ' \n' + text : ` — ${text}`);
}

export function plain(parts: Proto.SymbolDisplayPart[] | string): string {
return processInlineTags(
typeof parts === 'string'
? parts
: parts.map(part => part.text).join(''));
export function plainWithLinks(parts: Proto.SymbolDisplayPart[] | string): string {
return processInlineTags(convertLinkTags(parts));
}

/**
* Convert `@link` inline tags to markdown links
*/
function convertLinkTags(parts: Proto.SymbolDisplayPart[] | string | undefined): string {
if (!parts) {
return '';
}

if (typeof parts === 'string') {
return parts;
}

const out: string[] = [];

let currentLink: { name?: string, target?: Proto.FileSpan, text?: string } | undefined;
for (const part of parts) {
switch (part.kind) {
case 'link':
if (currentLink) {
if (currentLink.target) {
const commandUri = OpenJsDocLinkCommand.createCommandUri(currentLink.target);
out.push(`[${currentLink.text ?? currentLink.name}](${commandUri})`);
}
currentLink = undefined;
} else {
currentLink = {};
}
break;

case 'linkName':
if (currentLink) {
currentLink.name = part.text;
currentLink.target = (part as Proto.JSDocLinkDisplayPart).target;
}
break;

case 'linkText':
if (currentLink) {
currentLink.text = part.text;
}
break;

default:
out.push(part.text);
break;
}
}
return processInlineTags(out.join(''));
}

export function tagsMarkdownPreview(tags: Proto.JSDocTagInfo[]): string {
Expand All @@ -105,6 +154,9 @@ export function markdownDocumentation(
tags: Proto.JSDocTagInfo[]
): vscode.MarkdownString {
const out = new vscode.MarkdownString();
out.isTrusted = {
trustedCommands: [OpenJsDocLinkCommand.id]
};
addMarkdownDocumentation(out, documentation, tags);
return out;
}
Expand All @@ -115,7 +167,7 @@ export function addMarkdownDocumentation(
tags: Proto.JSDocTagInfo[] | undefined
): vscode.MarkdownString {
if (documentation) {
out.appendMarkdown(plain(documentation));
out.appendMarkdown(plainWithLinks(documentation));
}

if (tags) {
Expand Down
8 changes: 4 additions & 4 deletions extensions/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ fast-plist@0.1.2:
resolved "https://registry.yarnpkg.com/fast-plist/-/fast-plist-0.1.2.tgz#a45aff345196006d406ca6cdcd05f69051ef35b8"
integrity sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg=

typescript@4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3"
integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==
typescript@^4.3.0-dev.20210318:
version "4.3.0-dev.20210318"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.0-dev.20210318.tgz#3a6def444ace2a21d3b18573feed4ac67cb4b671"
integrity sha512-wLmMZTXjGFoqLPU63244P1umXxDVK/7KMxZ9ZpjU4GUgawJQGFywGrnmbdeNc2f1pySIJXWCBtDMY9QWIupriQ==

vscode-grammar-updater@^1.0.3:
version "1.0.3"
Expand Down
17 changes: 15 additions & 2 deletions src/vs/base/browser/markdownRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,17 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
try {
const href = target.dataset['href'];
if (href) {
const uri = URI.parse(href);
if (uri.scheme === Schemas.command) {
if (!markdown.isTrusted) {
return;
}
if (typeof markdown.isTrusted === 'object') {
if (!markdown.isTrusted.trustedCommands?.some(trustedCommand => trustedCommand === uri.path)) {
return;
}
}
}
options.actionHandler!.callback(href, mouseEvent);
}
} catch (err) {
Expand Down Expand Up @@ -259,14 +270,16 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
}

function sanitizeRenderedMarkdown(
options: { isTrusted?: boolean },
options: { isTrusted?: boolean | { readonly trustedCommands: readonly string[] } },
renderedMarkdown: string,
): string | TrustedHTML {
const insaneOptions = getInsaneOptions(options);
return _ttpInsane?.createHTML(renderedMarkdown, insaneOptions) ?? insane(renderedMarkdown, insaneOptions);
}

function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOptions {
function getInsaneOptions(options: {
readonly isTrusted?: boolean | { readonly trustedCommands: readonly string[] }
}): InsaneOptions {
const allowedSchemes = [
Schemas.http,
Schemas.https,
Expand Down
10 changes: 7 additions & 3 deletions src/vs/base/common/htmlContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ import { UriComponents } from 'vs/base/common/uri';
import { escapeIcons } from 'vs/base/common/iconLabels';
import { illegalArgument } from 'vs/base/common/errors';

export interface IMarkdownStringTrustSettings {
readonly trustedCommands: readonly string[];
}

export interface IMarkdownString {
readonly value: string;
readonly isTrusted?: boolean;
readonly isTrusted?: boolean | IMarkdownStringTrustSettings;
readonly supportThemeIcons?: boolean;
uris?: { [href: string]: UriComponents };
}
Expand All @@ -23,7 +27,7 @@ export const enum MarkdownStringTextNewlineStyle {
export class MarkdownString implements IMarkdownString {

public value: string;
public isTrusted?: boolean;
public isTrusted?: boolean | IMarkdownStringTrustSettings;
public supportThemeIcons?: boolean;

constructor(
Expand Down Expand Up @@ -84,7 +88,7 @@ export function isMarkdownString(thing: any): thing is IMarkdownString {
return true;
} else if (thing && typeof thing === 'object') {
return typeof (<IMarkdownString>thing).value === 'string'
&& (typeof (<IMarkdownString>thing).isTrusted === 'boolean' || (<IMarkdownString>thing).isTrusted === undefined)
&& (typeof (<IMarkdownString>thing).isTrusted === 'boolean' || (<IMarkdownString>thing).isTrusted === undefined || (typeof (<IMarkdownString>thing).isTrusted === 'object' && Array.isArray(((<IMarkdownString>thing).isTrusted as IMarkdownStringTrustSettings).trustedCommands)))
&& (typeof (<IMarkdownString>thing).supportThemeIcons === 'boolean' || (<IMarkdownString>thing).supportThemeIcons === undefined);
}
return false;
Expand Down
Loading

0 comments on commit c4ced96

Please sign in to comment.