No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 

239 líneas
7.0 KiB

'use strict';
import { ConfigurationChangeEvent, Disposable } from 'vscode';
import { AutolinkReference, configuration } from '../configuration';
import { Container } from '../container';
import { Dates, debug, Iterables, Promises, Strings } from '../system';
import { Logger } from '../logger';
import { GitRemote, IssueOrPullRequest } from '../git/git';
import { GlyphChars } from '../constants';
const numRegex = /<num>/g;
export interface CacheableAutolinkReference extends AutolinkReference {
linkify?: ((text: string, markdown: boolean, footnotes?: Map<number, string>) => string) | null;
messageMarkdownRegex?: RegExp;
messageRegex?: RegExp;
}
export interface DynamicAutolinkReference {
linkify: (text: string, markdown: boolean, footnotes?: Map<number, string>) => string;
}
function isDynamic(ref: AutolinkReference | DynamicAutolinkReference): ref is DynamicAutolinkReference {
return (ref as AutolinkReference).prefix === undefined && (ref as AutolinkReference).url === undefined;
}
function isCacheable(ref: AutolinkReference | DynamicAutolinkReference): ref is CacheableAutolinkReference {
return (ref as AutolinkReference).prefix !== undefined && (ref as AutolinkReference).url !== undefined;
}
export class Autolinks implements Disposable {
protected _disposable: Disposable | undefined;
private _references: CacheableAutolinkReference[] = [];
constructor() {
this._disposable = Disposable.from(configuration.onDidChange(this.onConfigurationChanged, this));
this.onConfigurationChanged(configuration.initializingChangeEvent);
}
dispose() {
this._disposable?.dispose();
}
private onConfigurationChanged(e: ConfigurationChangeEvent) {
if (configuration.changed(e, 'autolinks')) {
this._references = Container.config.autolinks ?? [];
}
}
@debug({
args: {
0: (message: string) => message.substring(0, 50),
1: _ => false,
2: ({ timeout }) => timeout,
},
})
async getIssueOrPullRequestLinks(message: string, remote: GitRemote, { timeout }: { timeout?: number } = {}) {
if (!remote.provider?.hasApi()) return undefined;
const { provider } = remote;
const connected = provider.maybeConnected ?? (await provider.isConnected());
if (!connected) return undefined;
const ids = new Set<string>();
let match;
let num;
for (const ref of provider.autolinks) {
if (!isCacheable(ref)) continue;
if (ref.messageRegex === undefined) {
ref.messageRegex = new RegExp(
`(?<=^|\\s|\\(|\\\\\\[)(${Strings.escapeRegex(ref.prefix)}([${
ref.alphanumeric ? '\\w' : '0-9'
}]+))\\b`,
ref.ignoreCase ? 'gi' : 'g',
);
}
do {
match = ref.messageRegex.exec(message);
if (match == null) break;
[, , num] = match;
ids.add(num);
} while (true);
}
if (ids.size === 0) return undefined;
const issuesOrPullRequests = await Promises.raceAll(
ids.values(),
id => provider.getIssueOrPullRequest(id),
timeout,
);
if (issuesOrPullRequests.size === 0 || Iterables.every(issuesOrPullRequests.values(), pr => pr === undefined)) {
return undefined;
}
return issuesOrPullRequests;
}
@debug({
args: {
0: (text: string) => text.substring(0, 30),
2: _ => false,
3: _ => false,
},
})
linkify(
text: string,
markdown: boolean,
remotes?: GitRemote[],
issuesOrPullRequests?: Map<string, IssueOrPullRequest | Promises.CancellationError | undefined>,
footnotes?: Map<number, string>,
) {
for (const ref of this._references) {
if (this.ensureAutolinkCached(ref, issuesOrPullRequests)) {
if (ref.linkify != null) {
text = ref.linkify(text, markdown, footnotes);
}
}
}
if (remotes != null && remotes.length !== 0) {
for (const r of remotes) {
if (r.provider === undefined) continue;
for (const ref of r.provider.autolinks) {
if (this.ensureAutolinkCached(ref, issuesOrPullRequests)) {
if (ref.linkify != null) {
text = ref.linkify(text, markdown, footnotes);
}
}
}
}
}
return text;
}
private ensureAutolinkCached(
ref: CacheableAutolinkReference | DynamicAutolinkReference,
issuesOrPullRequests?: Map<string, IssueOrPullRequest | Promises.CancellationError | undefined>,
): ref is CacheableAutolinkReference | DynamicAutolinkReference {
if (isDynamic(ref)) return true;
try {
if (ref.messageMarkdownRegex === undefined) {
ref.messageMarkdownRegex = new RegExp(
`(?<=^|\\s|\\(|\\\\\\[)(${Strings.escapeRegex(Strings.escapeMarkdown(ref.prefix))}([${
ref.alphanumeric ? '\\w' : '0-9'
}]+))\\b`,
ref.ignoreCase ? 'gi' : 'g',
);
}
if (issuesOrPullRequests == null || issuesOrPullRequests.size === 0) {
const replacement = `[$1](${ref.url.replace(numRegex, '$2')}${
ref.title ? ` "${ref.title.replace(numRegex, '$2')}"` : ''
})`;
ref.linkify = (text: string, markdown: boolean) =>
markdown ? text.replace(ref.messageMarkdownRegex!, replacement) : text;
return true;
}
ref.linkify = (text: string, markdown: boolean, footnotes?: Map<number, string>) => {
if (markdown) {
return text.replace(ref.messageMarkdownRegex!, (_substring, linkText, num) => {
const issue = issuesOrPullRequests?.get(num);
let title = '';
if (ref.title) {
title = ` "${ref.title.replace(numRegex, num)}`;
if (issue != null) {
if (issue instanceof Promises.CancellationError) {
title += `\n${GlyphChars.Dash.repeat(2)}\nDetails timed out`;
} else {
title += `\n${GlyphChars.Dash.repeat(2)}\n${issue.title.replace(
/([")\\])/g,
'\\$1',
)}\n${issue.closed ? 'Closed' : 'Opened'}, ${Dates.getFormatter(
issue.closedDate ?? issue.date,
).fromNow()}`;
}
}
title += '"';
}
return `[${linkText}](${ref.url.replace(numRegex, num)}${title})`;
});
}
const includeFootnotes = footnotes == null;
let index;
text = text.replace(ref.messageRegex!, (_substring, linkText, num) => {
const issue = issuesOrPullRequests?.get(num);
if (issue == null) return linkText;
if (footnotes === undefined) {
footnotes = new Map<number, string>();
}
index = footnotes.size + 1;
footnotes.set(
footnotes.size + 1,
`${linkText}: ${
issue instanceof Promises.CancellationError
? 'Details timed out'
: `${issue.title} ${GlyphChars.Dot} ${
issue.closed ? 'Closed' : 'Opened'
}, ${Dates.getFormatter(issue.closedDate ?? issue.date).fromNow()}`
}`,
);
return `${linkText}${Strings.getSuperscript(index)}`;
});
return includeFootnotes && footnotes != null && footnotes.size !== 0
? `${text}\n${GlyphChars.Dash.repeat(2)}\n${Iterables.join(
Iterables.map(footnotes, ([i, footnote]) => `${Strings.getSuperscript(i)} ${footnote}`),
'\n',
)}`
: text;
};
} catch (ex) {
Logger.error(
ex,
`Failed to create autolink generator: prefix=${ref.prefix}, url=${ref.url}, title=${ref.title}`,
);
ref.linkify = null;
}
return true;
}
}