Path: blob/main/extensions/copilot/src/extension/linkify/test/node/linkifier.spec.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { suite, test } from 'vitest';6import { CancellationToken } from '../../../../util/vs/base/common/cancellation';7import { coalesceParts, LinkifiedPart, LinkifyLocationAnchor } from '../../common/linkifiedText';8import { ILinkifier, LinkifierContext } from '../../common/linkifyService';9import { assertPartsEqual, createTestLinkifierService, workspaceFile } from './util';1011const emptyContext: LinkifierContext = { requestId: undefined, references: [] };1213suite('Stateful Linkifier', () => {1415async function runLinkifier(linkifier: ILinkifier, parts: readonly string[]): Promise<LinkifiedPart[]> {16const out: LinkifiedPart[] = [];17for (const part of parts) {18out.push(...(await linkifier.append(part, CancellationToken.None)).parts);19}2021out.push(...(await linkifier.flush(CancellationToken.None))?.parts ?? []);22return coalesceParts(out);23}2425test(`Should not linkify inside of markdown code blocks`, async () => {26const linkifier = createTestLinkifierService(27'file.ts',28'src/file.ts',29).createLinkifier(emptyContext);3031const parts: string[] = [32'[file.ts](file.ts)',33'\n',34'```',35'\n',36'[file.ts](file.ts)',37'\n',38'```',39'\n',40'[file.ts](file.ts)',41];4243const result = await runLinkifier(linkifier, parts);44assertPartsEqual(result, [45new LinkifyLocationAnchor(workspaceFile('file.ts')),46['\n',47'```',48'\n',49'[file.ts](file.ts)', // no linkification here50'\n',51'```',52'\n'53].join(''),54new LinkifyLocationAnchor(workspaceFile('file.ts'))55]);56});5758test(`Should handle link tokens`, async () => {59const linkifier = createTestLinkifierService(60'file.ts',61'src/file.ts',62).createLinkifier(emptyContext);6364{65// Tokens for `[file.ts](file.ts)`66const parts: string[] = [67'[file',68'.ts',69'](',70'file',71'.ts',72')',73];7475const result = await runLinkifier(linkifier, parts);76assertPartsEqual(result, [77new LinkifyLocationAnchor(workspaceFile('file.ts')),78]);79}80{81// Another potential tokenization for `[file.ts](file.ts)`82const parts: string[] = [83'[',84'file',85'.ts',86'](',87'file',88'.ts',89')',90];9192const result = await runLinkifier(linkifier, parts);93assertPartsEqual(result, [94new LinkifyLocationAnchor(workspaceFile('file.ts')),95]);96}97{98// With leading space potential tokenization for `[file.ts](file.ts)`99const parts: string[] = [100' [',101'file',102'.ts',103'](',104'file',105'.ts',106')',107];108109const result = await runLinkifier(linkifier, parts);110assertPartsEqual(result, [111' ',112new LinkifyLocationAnchor(workspaceFile('file.ts')),113]);114}115});116117test(`Should handle inline code with spaces`, async () => {118const linkText = 'LINK';119120const linkifier = createTestLinkifierService(121'file.ts',122'src/file.ts',123).createLinkifier(emptyContext, [124{125create: () => ({126async linkify(newText) {127if (/\s*`[^`]+`\s*/.test(newText)) {128return { parts: [linkText] };129}130return;131},132})133}134]);135136const parts: string[] = [137'`code ',138' more`',139];140141const result = await runLinkifier(linkifier, parts);142assertPartsEqual(result, [143linkText144]);145});146147test(`Should not linkify inside of markdown fenced code block containing fenced code blocks (#5708)`, async () => {148const linkifier = createTestLinkifierService(149'file.ts',150).createLinkifier(emptyContext);151152const parts: string[] = [153'[file.ts](file.ts)',154'\n',155'```md',156'\n',157'[file.ts](file.ts)',158'\n',159'\t```ts',160'\n',161`\t1 + 1`,162'\n',163'\t[file.ts](file.ts)',164'\n',165'\t```',166'\n',167'[file.ts](file.ts)',168'\n',169'```',170'\n',171'[file.ts](file.ts)',172];173174const result = await runLinkifier(linkifier, parts);175assertPartsEqual(result, [176new LinkifyLocationAnchor(workspaceFile('file.ts')),177[178'\n',179'```md',180'\n',181'[file.ts](file.ts)', // No linkification182'\n',183'\t```ts',184'\n',185`\t1 + 1`,186'\n',187'\t[file.ts](file.ts)', // No linkification188'\n',189'\t```',190'\n',191'[file.ts](file.ts)', // No linkification192'\n',193'```',194'\n',195].join(''),196new LinkifyLocationAnchor(workspaceFile('file.ts'))197]);198});199200test(`Should not linkify inside tilde markdown code blocks`, async () => {201const linkifier = createTestLinkifierService(202'file.ts',203).createLinkifier(emptyContext);204205const parts: string[] = [206'[file.ts](file.ts)',207'\n',208'~~~',209'\n',210'[file.ts](file.ts)',211'\n',212'~~~',213'\n',214'[file.ts](file.ts)',215];216217const result = await runLinkifier(linkifier, parts);218assertPartsEqual(result, [219new LinkifyLocationAnchor(workspaceFile('file.ts')),220[221'\n',222'~~~',223'\n',224'[file.ts](file.ts)', // no linkification here225'\n',226'~~~',227'\n',228].join(''),229new LinkifyLocationAnchor(workspaceFile('file.ts'))230]);231});232233test(`Should correctly handle fenced code blocks split over multiple parts`, async () => {234const linkifier = createTestLinkifierService(235'file.ts',236).createLinkifier(emptyContext);237238const parts: string[] = [239'[file.ts](file.ts)',240'\n',241'```ts',242'\n',243'[file.ts](file.ts)',244'\n``', // Split ending backtick245'`',246'\n',247'[file.ts](file.ts)',248];249250const result = await runLinkifier(linkifier, parts);251assertPartsEqual(result, [252new LinkifyLocationAnchor(workspaceFile('file.ts')),253[254'\n',255'```ts',256'\n',257'[file.ts](file.ts)', // no linkification here258'\n',259'```',260'\n',261].join(''),262new LinkifyLocationAnchor(workspaceFile('file.ts'))263]);264});265266test(`Should correctly handle fenced code blocks when opening fence is split`, async () => {267const linkifier = createTestLinkifierService(268'file.ts',269).createLinkifier(emptyContext);270271const parts: string[] = [272'[file.ts](file.ts)',273'\n',274'``', // Split opening backticks275'`ts',276'\n',277'[file.ts](file.ts)',278'\n``', // Split ending backtick279'`',280'\n',281'[file.ts](file.ts)',282];283284const result = await runLinkifier(linkifier, parts);285assertPartsEqual(result, [286new LinkifyLocationAnchor(workspaceFile('file.ts')),287[288'\n',289'```ts',290'\n',291'[file.ts](file.ts)', // no linkification here292'\n',293'```',294'\n',295].join(''),296new LinkifyLocationAnchor(workspaceFile('file.ts'))297]);298});299300test(`Should de-linkify links without schemes`, async () => {301const linkifier = createTestLinkifierService().createLinkifier(emptyContext);302303const parts: string[] = [304'[text](file.ts) [`text`](/file.ts)',305];306307const result = await runLinkifier(linkifier, parts);308assertPartsEqual(result, [309'text `text`'310]);311});312313test(`Should de-linkify Windows absolute paths with drive letters`, async () => {314const linkifier = createTestLinkifierService().createLinkifier(emptyContext);315316const parts: string[] = [317'[text](c:\\src\\file.ts)',318];319320const result = await runLinkifier(linkifier, parts);321assertPartsEqual(result, [322'text'323]);324});325326test(`Should not unlinkify text inside of code blocks`, async () => {327const linkifier = createTestLinkifierService().createLinkifier(emptyContext);328329const parts: string[] = [330'```md\n',331`[g](x)\n`,332'```',333];334335const result = await runLinkifier(linkifier, parts);336assertPartsEqual(result, [337[338'```md\n',339`[g](x)\n`,340'```'341].join('')342]);343});344345test(`Should not unlikify text inside of inline code`, async () => {346{347const linkifier = createTestLinkifierService().createLinkifier(emptyContext);348const result = await runLinkifier(linkifier, [349'a `J[g](x)` b',350]);351assertPartsEqual(result, [352'a `J[g](x)` b'353]);354}355{356const linkifier = createTestLinkifierService().createLinkifier(emptyContext);357const result = await runLinkifier(linkifier, [358'a `b [c](d) e` f',359]);360assertPartsEqual(result, [361'a `b [c](d) e` f'362]);363}364});365366test(`Should not unlikify text inside of math blocks code`, async () => {367{368const linkifier = createTestLinkifierService(369'file1.ts',370'file2.ts',371).createLinkifier(emptyContext);372373const result = await runLinkifier(linkifier, [374'[file1.ts](file1.ts)\n',375'$$\n',376`J[g](x)\n`,377'$$\n',378'[file2.ts](file2.ts)'379]);380assertPartsEqual(result, [381new LinkifyLocationAnchor(workspaceFile('file1.ts')),382[383'',384'$$',385'J[g](x)',386'$$',387'',388].join('\n'),389new LinkifyLocationAnchor(workspaceFile('file2.ts')),390]);391}392});393394test(`Should not touch code inside of inline math equations`, async () => {395{396const linkifier = createTestLinkifierService().createLinkifier(emptyContext);397398const result = await runLinkifier(linkifier, [399'a $J[g](x)$ b',400]);401assertPartsEqual(result, [402'a $J[g](x)$ b'403]);404}405{406const linkifier = createTestLinkifierService().createLinkifier(emptyContext);407408const result = await runLinkifier(linkifier, [409'a $c [g](x) d$ x',410]);411assertPartsEqual(result, [412'a $c [g](x) d$ x',413]);414}415});416});417418419