Skip to content

Commit ff6c760

Browse files
committed
Separate utils from UI dependent code
1 parent f18105e commit ff6c760

File tree

4 files changed

+298
-275
lines changed

4 files changed

+298
-275
lines changed

src/autolinks/__tests__/autolinks.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as assert from 'assert';
22
import { suite, test } from 'mocha';
33
import { map } from '../../system/iterable';
4-
import type { Autolink, RefSet } from '../autolinks';
5-
import { getAutolinks, getBranchAutolinks } from '../autolinks';
4+
import type { Autolink, RefSet } from '../autolinks.utils';
5+
import { getAutolinks, getBranchAutolinks } from '../autolinks.utils';
66

77
const mockRefSets = (prefixes: string[] = ['']): RefSet[] =>
88
prefixes.map(prefix => [

src/autolinks/autolinks.ts

Lines changed: 18 additions & 273 deletions
Original file line numberDiff line numberDiff line change
@@ -4,144 +4,37 @@ import { GlyphChars } from '../constants';
44
import type { IntegrationId } from '../constants.integrations';
55
import { IssueIntegrationId } from '../constants.integrations';
66
import type { Container } from '../container';
7-
import type { IssueOrPullRequest } from '../git/models/issue';
87
import type { GitRemote } from '../git/models/remote';
9-
import type { ProviderReference } from '../git/models/remoteProvider';
108
import { getIssueOrPullRequestHtmlIcon, getIssueOrPullRequestMarkdownIcon } from '../git/utils/icons';
11-
import type { HostingIntegration, IssueIntegration, ResourceDescriptor } from '../plus/integrations/integration';
9+
import type { HostingIntegration, IssueIntegration } from '../plus/integrations/integration';
1210
import { fromNow } from '../system/date';
1311
import { debug } from '../system/decorators/log';
1412
import { encodeUrl } from '../system/encoding';
1513
import { join, map } from '../system/iterable';
1614
import { Logger } from '../system/logger';
1715
import { escapeMarkdown } from '../system/markdown';
18-
import type { MaybePausedResult } from '../system/promise';
1916
import { getSettledValue, isPromise } from '../system/promise';
20-
import { capitalize, encodeHtmlWeak, escapeRegex, getSuperscript } from '../system/string';
17+
import { capitalize, encodeHtmlWeak, getSuperscript } from '../system/string';
2118
import { configuration } from '../system/vscode/configuration';
19+
import type {
20+
Autolink,
21+
CacheableAutolinkReference,
22+
DynamicAutolinkReference,
23+
EnrichedAutolink,
24+
MaybeEnrichedAutolink,
25+
RefSet,
26+
} from './autolinks.utils';
27+
import {
28+
ensureCachedRegex,
29+
getAutolinks,
30+
getBranchAutolinks,
31+
isDynamic,
32+
numRegex,
33+
supportedAutolinkIntegrations,
34+
} from './autolinks.utils';
2235

2336
const emptyAutolinkMap = Object.freeze(new Map<string, Autolink>());
2437

25-
const numRegex = /<num>/g;
26-
27-
export type AutolinkType = 'issue' | 'pullrequest';
28-
export type AutolinkReferenceType = 'commit' | 'branch';
29-
30-
export interface AutolinkReference {
31-
/** Short prefix to match to generate autolinks for the external resource */
32-
readonly prefix: string;
33-
/** URL of the external resource to link to */
34-
readonly url: string;
35-
/** Whether alphanumeric characters should be allowed in `<num>` */
36-
readonly alphanumeric: boolean;
37-
/** Whether case should be ignored when matching the prefix */
38-
readonly ignoreCase: boolean;
39-
readonly title: string | undefined;
40-
41-
readonly type?: AutolinkType;
42-
readonly referenceType?: AutolinkReferenceType;
43-
readonly description?: string;
44-
readonly descriptor?: ResourceDescriptor;
45-
}
46-
47-
export interface Autolink extends AutolinkReference {
48-
provider?: ProviderReference;
49-
id: string;
50-
index?: number;
51-
52-
tokenize?:
53-
| ((
54-
text: string,
55-
outputFormat: 'html' | 'markdown' | 'plaintext',
56-
tokenMapping: Map<string, string>,
57-
enrichedAutolinks?: Map<string, MaybeEnrichedAutolink>,
58-
prs?: Set<string>,
59-
footnotes?: Map<number, string>,
60-
) => string)
61-
| null;
62-
}
63-
64-
export type EnrichedAutolink = [
65-
issueOrPullRequest: Promise<IssueOrPullRequest | undefined> | undefined,
66-
autolink: Autolink,
67-
];
68-
69-
export type MaybeEnrichedAutolink = readonly [
70-
issueOrPullRequest: MaybePausedResult<IssueOrPullRequest | undefined> | undefined,
71-
autolink: Autolink,
72-
];
73-
74-
export function serializeAutolink(value: Autolink): Autolink {
75-
const serialized: Autolink = {
76-
provider: value.provider
77-
? {
78-
id: value.provider.id,
79-
name: value.provider.name,
80-
domain: value.provider.domain,
81-
icon: value.provider.icon,
82-
}
83-
: undefined,
84-
id: value.id,
85-
index: value.index,
86-
prefix: value.prefix,
87-
url: value.url,
88-
alphanumeric: value.alphanumeric,
89-
ignoreCase: value.ignoreCase,
90-
title: value.title,
91-
type: value.type,
92-
description: value.description,
93-
descriptor: value.descriptor,
94-
};
95-
return serialized;
96-
}
97-
98-
export interface CacheableAutolinkReference extends AutolinkReference {
99-
tokenize?:
100-
| ((
101-
text: string,
102-
outputFormat: 'html' | 'markdown' | 'plaintext',
103-
tokenMapping: Map<string, string>,
104-
enrichedAutolinks?: Map<string, MaybeEnrichedAutolink>,
105-
prs?: Set<string>,
106-
footnotes?: Map<number, string>,
107-
) => string)
108-
| null;
109-
110-
messageHtmlRegex?: RegExp;
111-
messageMarkdownRegex?: RegExp;
112-
messageRegex?: RegExp;
113-
branchNameRegex?: RegExp;
114-
}
115-
116-
export interface DynamicAutolinkReference {
117-
tokenize?:
118-
| ((
119-
text: string,
120-
outputFormat: 'html' | 'markdown' | 'plaintext',
121-
tokenMapping: Map<string, string>,
122-
enrichedAutolinks?: Map<string, MaybeEnrichedAutolink>,
123-
prs?: Set<string>,
124-
footnotes?: Map<number, string>,
125-
) => string)
126-
| null;
127-
parse: (text: string, autolinks: Map<string, Autolink>) => void;
128-
}
129-
130-
export const supportedAutolinkIntegrations = [IssueIntegrationId.Jira];
131-
132-
function isDynamic(ref: AutolinkReference | DynamicAutolinkReference): ref is DynamicAutolinkReference {
133-
return !('prefix' in ref) && !('url' in ref);
134-
}
135-
136-
function isCacheable(ref: AutolinkReference | DynamicAutolinkReference): ref is CacheableAutolinkReference {
137-
return 'prefix' in ref && ref.prefix != null && 'url' in ref && ref.url != null;
138-
}
139-
140-
export type RefSet = [
141-
ProviderReference | undefined,
142-
(AutolinkReference | DynamicAutolinkReference)[] | CacheableAutolinkReference[],
143-
];
144-
14538
export class Autolinks implements Disposable {
14639
protected _disposable: Disposable | undefined;
14740
private _references: CacheableAutolinkReference[] = [];
@@ -629,151 +522,3 @@ export class Autolinks implements Disposable {
629522
return true;
630523
}
631524
}
632-
633-
function ensureCachedRegex(
634-
ref: CacheableAutolinkReference,
635-
outputFormat: 'html',
636-
): asserts ref is RequireSome<CacheableAutolinkReference, 'messageHtmlRegex'>;
637-
function ensureCachedRegex(
638-
ref: CacheableAutolinkReference,
639-
outputFormat: 'markdown',
640-
): asserts ref is RequireSome<CacheableAutolinkReference, 'messageMarkdownRegex'>;
641-
function ensureCachedRegex(
642-
ref: CacheableAutolinkReference,
643-
outputFormat: 'plaintext',
644-
): asserts ref is RequireSome<CacheableAutolinkReference, 'messageRegex' | 'branchNameRegex'>;
645-
function ensureCachedRegex(ref: CacheableAutolinkReference, outputFormat: 'html' | 'markdown' | 'plaintext') {
646-
// Regexes matches the ref prefix followed by a token (e.g. #1234)
647-
if (outputFormat === 'markdown' && ref.messageMarkdownRegex == null) {
648-
// Extra `\\\\` in `\\\\\\[` is because the markdown is escaped
649-
ref.messageMarkdownRegex = new RegExp(
650-
`(^|\\s|\\(|\\[|\\{)(${escapeRegex(encodeHtmlWeak(escapeMarkdown(ref.prefix)))}(${
651-
ref.alphanumeric ? '\\w' : '\\d'
652-
}+))\\b`,
653-
ref.ignoreCase ? 'gi' : 'g',
654-
);
655-
} else if (outputFormat === 'html' && ref.messageHtmlRegex == null) {
656-
ref.messageHtmlRegex = new RegExp(
657-
`(^|\\s|\\(|\\[|\\{)(${escapeRegex(encodeHtmlWeak(ref.prefix))}(${ref.alphanumeric ? '\\w' : '\\d'}+))\\b`,
658-
ref.ignoreCase ? 'gi' : 'g',
659-
);
660-
} else if (ref.messageRegex == null) {
661-
ref.messageRegex = new RegExp(
662-
`(^|\\s|\\(|\\[|\\{)(${escapeRegex(ref.prefix)}(${ref.alphanumeric ? '\\w' : '\\d'}+))\\b`,
663-
ref.ignoreCase ? 'gi' : 'g',
664-
);
665-
ref.branchNameRegex = new RegExp(
666-
`(^|\\-|_|\\.|\\/)(?<prefix>${ref.prefix})(?<issueKeyNumber>${
667-
ref.alphanumeric ? '\\w' : '\\d'
668-
}+)(?=$|\\-|_|\\.|\\/)`,
669-
'gi',
670-
);
671-
}
672-
673-
return true;
674-
}
675-
676-
/**
677-
* Compares autolinks
678-
* @returns non-0 result that means a probability of the autolink `b` is more relevant of the autolink `a`
679-
*/
680-
function compareAutolinks(a: Autolink, b: Autolink): number {
681-
// consider that if the number is in the start, it's the most relevant link
682-
if (b.index === 0) return 1;
683-
if (a.index === 0) return -1;
684-
685-
// maybe it worths to use some weight function instead.
686-
return (
687-
b.prefix.length - a.prefix.length ||
688-
b.id.length - a.id.length ||
689-
(b.index != null && a.index != null ? -(b.index - a.index) : 0)
690-
);
691-
}
692-
693-
export function getAutolinks(message: string, refsets: Readonly<RefSet[]>) {
694-
const autolinks = new Map<string, Autolink>();
695-
696-
let match;
697-
let num;
698-
for (const [provider, refs] of refsets) {
699-
for (const ref of refs) {
700-
if (!isCacheable(ref) || (ref.referenceType && ref.referenceType !== 'commit')) {
701-
if (isDynamic(ref)) {
702-
ref.parse(message, autolinks);
703-
}
704-
continue;
705-
}
706-
707-
ensureCachedRegex(ref, 'plaintext');
708-
709-
do {
710-
match = ref.messageRegex.exec(message);
711-
if (!match) break;
712-
713-
[, , , num] = match;
714-
715-
autolinks.set(num, {
716-
provider: provider,
717-
id: num,
718-
index: match.index,
719-
prefix: ref.prefix,
720-
url: ref.url?.replace(numRegex, num),
721-
alphanumeric: ref.alphanumeric,
722-
ignoreCase: ref.ignoreCase,
723-
title: ref.title?.replace(numRegex, num),
724-
type: ref.type,
725-
description: ref.description?.replace(numRegex, num),
726-
descriptor: ref.descriptor,
727-
});
728-
} while (true);
729-
}
730-
}
731-
732-
return autolinks;
733-
}
734-
735-
export function getBranchAutolinks(branchName: string, refsets: Readonly<RefSet[]>) {
736-
const autolinks = new Map<string, Autolink>();
737-
738-
let match;
739-
let num;
740-
for (const [provider, refs] of refsets) {
741-
for (const ref of refs) {
742-
if (
743-
!isCacheable(ref) ||
744-
ref.type === 'pullrequest' ||
745-
(ref.referenceType && ref.referenceType !== 'branch')
746-
) {
747-
continue;
748-
}
749-
750-
ensureCachedRegex(ref, 'plaintext');
751-
const matches = branchName.matchAll(ref.branchNameRegex);
752-
do {
753-
match = matches.next();
754-
if (!match.value?.groups) break;
755-
756-
num = match?.value?.groups.issueKeyNumber;
757-
let index = match.value.index;
758-
const linkUrl = ref.url?.replace(numRegex, num);
759-
// strange case (I would say synthetic), but if we parse the link twice, use the most relevant of them
760-
const existingIndex = autolinks.get(linkUrl)?.index;
761-
if (existingIndex != null) {
762-
index = Math.min(index, existingIndex);
763-
}
764-
autolinks.set(linkUrl, {
765-
...ref,
766-
provider: provider,
767-
id: num,
768-
index: index,
769-
url: linkUrl,
770-
title: ref.title?.replace(numRegex, num),
771-
description: ref.description?.replace(numRegex, num),
772-
descriptor: ref.descriptor,
773-
});
774-
} while (!match.done);
775-
}
776-
}
777-
778-
return new Map([...autolinks.entries()].sort((a, b) => compareAutolinks(a[1], b[1])));
779-
}

0 commit comments

Comments
 (0)