|
|
@ -2,23 +2,35 @@ |
|
|
|
import { ConfigurationChangeEvent, Disposable } from 'vscode'; |
|
|
|
import { AutolinkReference, configuration } from '../configuration'; |
|
|
|
import { Container } from '../container'; |
|
|
|
import { Strings } from '../system'; |
|
|
|
import { Dates, Strings } from '../system'; |
|
|
|
import { Logger } from '../logger'; |
|
|
|
import { GitRemote } from '../git/git'; |
|
|
|
import { GitRemote, Issue } from '../git/git'; |
|
|
|
import { RemoteProviderWithApi } from '../git/remotes/provider'; |
|
|
|
import { GlyphChars } from '../constants'; |
|
|
|
|
|
|
|
const numRegex = /<num>/g; |
|
|
|
|
|
|
|
export interface CacheableAutolinkReference extends AutolinkReference { |
|
|
|
linkify?: ((text: string) => string) | null; |
|
|
|
markdownRegex?: RegExp; |
|
|
|
textRegex?: RegExp; |
|
|
|
} |
|
|
|
|
|
|
|
export interface DynamicAutolinkReference { |
|
|
|
linkify: (text: string) => string; |
|
|
|
} |
|
|
|
|
|
|
|
function requiresGenerator(ref: AutolinkReference | DynamicAutolinkReference): ref is AutolinkReference { |
|
|
|
return ref.linkify === undefined; |
|
|
|
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: AutolinkReference[] = []; |
|
|
|
private _references: CacheableAutolinkReference[] = []; |
|
|
|
|
|
|
|
constructor() { |
|
|
|
this._disposable = Disposable.from(configuration.onDidChange(this.onConfigurationChanged, this)); |
|
|
@ -36,14 +48,51 @@ export class Autolinks implements Disposable { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
linkify(text: string, remotes?: GitRemote[]) { |
|
|
|
for (const ref of this._references) { |
|
|
|
if (requiresGenerator(ref)) { |
|
|
|
ref.linkify = this._getAutolinkGenerator(ref); |
|
|
|
async getIssueLinks(text: string, remotes: GitRemote[]) { |
|
|
|
if (remotes.length === 0) return undefined; |
|
|
|
|
|
|
|
const issues = new Map<number, Issue>(); |
|
|
|
|
|
|
|
for (const r of remotes) { |
|
|
|
if (!(r.provider instanceof RemoteProviderWithApi)) continue; |
|
|
|
if (!(await r.provider.isConnected())) continue; |
|
|
|
|
|
|
|
for (const ref of r.provider.autolinks) { |
|
|
|
if (!isCacheable(ref)) continue; |
|
|
|
|
|
|
|
if (ref.textRegex === undefined) { |
|
|
|
ref.textRegex = new RegExp(`(?<=^|\\s)(${ref.prefix}([0-9]+))\\b`, 'g'); |
|
|
|
} |
|
|
|
|
|
|
|
let match; |
|
|
|
let num; |
|
|
|
let id; |
|
|
|
let issue; |
|
|
|
do { |
|
|
|
match = ref.textRegex.exec(text); |
|
|
|
if (match == null) break; |
|
|
|
|
|
|
|
[, , num] = match; |
|
|
|
|
|
|
|
id = Number(num); |
|
|
|
issue = await r.provider.getIssue(id); |
|
|
|
if (issue != null) { |
|
|
|
issues.set(id, issue); |
|
|
|
} |
|
|
|
} while (true); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (ref.linkify != null) { |
|
|
|
text = ref.linkify(text); |
|
|
|
if (issues.size === 0) return undefined; |
|
|
|
return issues; |
|
|
|
} |
|
|
|
|
|
|
|
linkify(text: string, remotes?: GitRemote[], issues?: Map<number, Issue>) { |
|
|
|
for (const ref of this._references) { |
|
|
|
if (this.ensureAutolinkCached(ref, issues)) { |
|
|
|
if (ref.linkify != null) { |
|
|
|
text = ref.linkify(text); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@ -52,12 +101,10 @@ export class Autolinks implements Disposable { |
|
|
|
if (r.provider === undefined) continue; |
|
|
|
|
|
|
|
for (const ref of r.provider.autolinks) { |
|
|
|
if (requiresGenerator(ref)) { |
|
|
|
ref.linkify = this._getAutolinkGenerator(ref); |
|
|
|
} |
|
|
|
|
|
|
|
if (ref.linkify != null) { |
|
|
|
text = ref.linkify(text); |
|
|
|
if (this.ensureAutolinkCached(ref, issues)) { |
|
|
|
if (ref.linkify != null) { |
|
|
|
text = ref.linkify(text); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -66,19 +113,53 @@ export class Autolinks implements Disposable { |
|
|
|
return text; |
|
|
|
} |
|
|
|
|
|
|
|
private _getAutolinkGenerator({ prefix, url, title }: AutolinkReference) { |
|
|
|
private ensureAutolinkCached( |
|
|
|
ref: CacheableAutolinkReference | DynamicAutolinkReference, |
|
|
|
issues?: Map<number, Issue> |
|
|
|
): ref is CacheableAutolinkReference | DynamicAutolinkReference { |
|
|
|
if (isDynamic(ref)) return true; |
|
|
|
|
|
|
|
try { |
|
|
|
const regex = new RegExp( |
|
|
|
`(?<=^|\\s)(${Strings.escapeMarkdown(prefix).replace(/\\/g, '\\\\')}([0-9]+))\\b`, |
|
|
|
'g' |
|
|
|
); |
|
|
|
const markdown = `[$1](${url.replace(numRegex, '$2')}${ |
|
|
|
title ? ` "${title.replace(numRegex, '$2')}"` : '' |
|
|
|
})`;
|
|
|
|
return (text: string) => text.replace(regex, markdown); |
|
|
|
if (ref.markdownRegex === undefined) { |
|
|
|
ref.markdownRegex = new RegExp( |
|
|
|
`(?<=^|\\s)(${Strings.escapeMarkdown(ref.prefix).replace(/\\/g, '\\\\')}([0-9]+))\\b`, |
|
|
|
'g' |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
if (issues == null || issues.size === 0) { |
|
|
|
const markdown = `[$1](${ref.url.replace(numRegex, '$2')}${ |
|
|
|
ref.title ? ` "${ref.title.replace(numRegex, '$2')}"` : '' |
|
|
|
})`;
|
|
|
|
ref.linkify = (text: string) => text.replace(ref.markdownRegex!, markdown); |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
ref.linkify = (text: string) => |
|
|
|
text.replace(ref.markdownRegex!, (substring, linkText, number) => { |
|
|
|
const issue = issues?.get(Number(number)); |
|
|
|
|
|
|
|
return `[${linkText}](${ref.url.replace(numRegex, number)}${ |
|
|
|
ref.title |
|
|
|
? ` "${ref.title.replace(numRegex, number)}${ |
|
|
|
issue |
|
|
|
? `\n${GlyphChars.Dash.repeat(2)}\n${issue.title.replace(/([")])/g, '\\$1')}\n${ |
|
|
|
issue.closed ? 'Closed' : 'Opened' |
|
|
|
}, ${Dates.getFormatter(issue.closedDate ?? issue.date).fromNow()}`
|
|
|
|
: '' |
|
|
|
}"`
|
|
|
|
: '' |
|
|
|
})`;
|
|
|
|
}); |
|
|
|
} catch (ex) { |
|
|
|
Logger.error(ex, `Failed to create autolink generator: prefix=${prefix}, url=${url}, title=${title}`); |
|
|
|
return null; |
|
|
|
Logger.error( |
|
|
|
ex, |
|
|
|
`Failed to create autolink generator: prefix=${ref.prefix}, url=${ref.url}, title=${ref.title}` |
|
|
|
); |
|
|
|
ref.linkify = null; |
|
|
|
} |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
} |