Skip to content

Commit a1aaa42

Browse files
authored
feat(snapshot): add document symbols and folding support (#764)
1 parent 5425e1c commit a1aaa42

File tree

7 files changed

+251
-7
lines changed

7 files changed

+251
-7
lines changed

packages/extension/src/extension.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import { TestTree } from './testTree'
2222
import { getTestData, TestFile } from './testTreeData'
2323
import { clearCachedRuntime, debounce, showVitestError } from './utils'
2424
import './polyfills'
25+
import { SnapshotEntryTool } from './snapshot/tools'
26+
import { SnapshotDocumentSymbolProvider } from './snapshot/documentSymbolProvider'
27+
import { SnapshotFoldingRangeProvider } from './snapshot/foldingRangeProvider'
2528

2629
export async function activate(context: vscode.ExtensionContext) {
2730
const extension = new VitestExtension(context)
@@ -320,6 +323,7 @@ class VitestExtension {
320323
'vitest.runtime',
321324
'deno.enabled',
322325
]
326+
const snapshotEntryTool = new SnapshotEntryTool()
323327

324328
this.disposables = [
325329
vscode.workspace.onDidChangeConfiguration((event) => {
@@ -336,7 +340,7 @@ class VitestExtension {
336340
}),
337341
),
338342
vscode.commands.registerCommand('vitest.openOutput', () => {
339-
log.openOuput()
343+
log.openOutput()
340344
}),
341345
vscode.commands.registerCommand('vitest.runRelatedTests', async (uri?: vscode.Uri) => {
342346
const currentUri = uri || vscode.window.activeTextEditor?.document.uri
@@ -537,6 +541,14 @@ class VitestExtension {
537541

538542
await this.defineTestProfiles(false)
539543
}),
544+
vscode.languages.registerDocumentSymbolProvider(
545+
{ language: 'vitest-snapshot' },
546+
new SnapshotDocumentSymbolProvider(snapshotEntryTool),
547+
),
548+
vscode.languages.registerFoldingRangeProvider(
549+
{ language: 'vitest-snapshot' },
550+
new SnapshotFoldingRangeProvider(snapshotEntryTool),
551+
),
540552
]
541553

542554
// if the config changes, re-define all test profiles

packages/extension/src/log.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,17 @@ export const log = {
8888
workspaceError: (folder: string, ...args: any[]) => {
8989
log.error(`[Workspace ${folder}]`, ...args)
9090
},
91-
openOuput() {
91+
openOutput() {
9292
channel.show()
9393
},
9494
} as const
9595

96-
let exitsts = false
96+
let exists = false
9797
function appendFile(log: string) {
98-
if (!exitsts) {
98+
if (!exists) {
9999
mkdirSync(dirname(logFile), { recursive: true })
100100
writeFileSync(logFile, '')
101-
exitsts = true
101+
exists = true
102102
}
103103
appendFileSync(logFile, `${log}\n`)
104104
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as vscode from 'vscode'
2+
import { createSnapshotSymbol, pushToDocumentSymbol, type SnapshotEntryTool } from './tools'
3+
4+
export class SnapshotDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
5+
private latestUri: string | undefined = undefined
6+
private latestVersion: number | undefined = undefined
7+
latestDocumentSymbols: vscode.DocumentSymbol[] = []
8+
constructor(private snapshotEntryTool: SnapshotEntryTool) {}
9+
provideDocumentSymbols(
10+
document: vscode.TextDocument,
11+
token: vscode.CancellationToken,
12+
): vscode.ProviderResult<vscode.DocumentSymbol[]> {
13+
if (this.latestUri === document.uri.toString() && this.latestVersion === document.version) {
14+
return this.latestDocumentSymbols
15+
}
16+
this.snapshotEntryTool.process(document, document.uri.toString(), document.version, token)
17+
if (token.isCancellationRequested) return null // cancelled
18+
19+
this.latestUri = document.uri.toString()
20+
this.latestVersion = document.version
21+
22+
const documentSymbols: vscode.DocumentSymbol[] = []
23+
forExportsSymbol: for (const entry of this.snapshotEntryTool.snapshotEntries) {
24+
let currentLevel: vscode.DocumentSymbol[] = documentSymbols
25+
let parent: vscode.DocumentSymbol[] | undefined
26+
27+
for (let i = 0; i < entry.breadcrumb.length; i++) {
28+
const existingSymbol = currentLevel.at(-1)
29+
if (!existingSymbol || existingSymbol.name !== entry.breadcrumb[i]) {
30+
const newSymbol = createSnapshotSymbol(entry.breadcrumb[i], entry, i)
31+
currentLevel.push(newSymbol)
32+
i + 1 < entry.breadcrumb.length && pushToDocumentSymbol(newSymbol, entry, i + 1)
33+
continue forExportsSymbol
34+
}
35+
parent = currentLevel
36+
currentLevel = existingSymbol.children
37+
}
38+
// last level - all breadcrumbs matched, create duplicate leaf
39+
;(parent || documentSymbols).push(
40+
createSnapshotSymbol(entry.breadcrumb.at(-1)!, entry, entry.breadcrumb.length - 1),
41+
)
42+
}
43+
return (this.latestDocumentSymbols = documentSymbols)
44+
}
45+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as vscode from 'vscode'
2+
import { type SnapshotEntryTool } from './tools'
3+
4+
export class SnapshotFoldingRangeProvider implements vscode.FoldingRangeProvider {
5+
private latestUri: string | undefined = undefined
6+
private latestVersion: number | undefined = undefined
7+
latestFoldingRanges: vscode.FoldingRange[] = []
8+
constructor(private snapshotEntryTool: SnapshotEntryTool) {}
9+
provideFoldingRanges(
10+
document: vscode.TextDocument,
11+
_: vscode.FoldingContext,
12+
token: vscode.CancellationToken,
13+
): vscode.ProviderResult<vscode.FoldingRange[]> {
14+
if (this.latestUri === document.uri.toString() && this.latestVersion === document.version) {
15+
return this.latestFoldingRanges
16+
}
17+
this.snapshotEntryTool.process(document, document.uri.toString(), document.version, token)
18+
if (token.isCancellationRequested) return null // cancelled
19+
20+
this.latestUri = document.uri.toString()
21+
this.latestVersion = document.version
22+
const foldingRanges: vscode.FoldingRange[] = []
23+
for (const symbol of this.snapshotEntryTool.snapshotEntries) {
24+
foldingRanges.push(
25+
new vscode.FoldingRange(
26+
document.positionAt(symbol.start).line,
27+
document.positionAt(symbol.end).line,
28+
vscode.FoldingRangeKind.Region,
29+
),
30+
)
31+
}
32+
return (this.latestFoldingRanges = foldingRanges)
33+
}
34+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import * as vscode from 'vscode'
2+
3+
const ExportSymbolRegex = /^exports\[`([^`]*)`\]/gm
4+
const RangeEndRegex = /`;$/m
5+
6+
export interface SnapshotEntry {
7+
name: string
8+
breadcrumb: [...describeName: string[], itName: string]
9+
start: number
10+
end: number
11+
fullRange: vscode.Range
12+
keyRange: vscode.Range
13+
}
14+
15+
export class SnapshotEntryTool {
16+
private latestUri: string | undefined = undefined
17+
private latestVersion: number | undefined = undefined
18+
snapshotEntries: SnapshotEntry[] = []
19+
process(
20+
document: vscode.TextDocument,
21+
uri: string,
22+
version: number,
23+
token: vscode.CancellationToken,
24+
): void {
25+
let changeUri = false
26+
let changeVersion = false
27+
if (this.latestUri !== uri) {
28+
this.latestUri = uri
29+
this.latestVersion = version
30+
changeUri = true
31+
changeVersion = true
32+
} else if (this.latestVersion !== version) {
33+
this.latestVersion = version
34+
changeVersion = true
35+
}
36+
37+
if (!changeUri && !changeVersion) {
38+
return // cached
39+
} else {
40+
// reset snapshotEntries
41+
this.snapshotEntries = []
42+
}
43+
if (token.isCancellationRequested) return // cancelled
44+
const text = document.getText()
45+
const exportsSymbols = text.matchAll(ExportSymbolRegex) || []
46+
47+
for (const match of exportsSymbols) {
48+
const name = match[1]
49+
const snapshotDataStart = match.index
50+
const snapshotDataEnd =
51+
snapshotDataStart +
52+
// find the nearest closing delimiter
53+
(text.slice(snapshotDataStart).match(RangeEndRegex)?.index ??
54+
// fallback to empty snapshot
55+
'exports[`'.length + name.length + '`]'.length + ' = `'.length + '""'.length) +
56+
'`;'.length
57+
58+
this.snapshotEntries.push({
59+
name: name,
60+
breadcrumb: name.split(' > ') as [...describeName: string[], itName: string],
61+
start: snapshotDataStart,
62+
end: snapshotDataEnd,
63+
fullRange: new vscode.Range(
64+
document.positionAt(snapshotDataStart),
65+
document.positionAt(snapshotDataEnd),
66+
),
67+
keyRange: new vscode.Range(
68+
document.positionAt(snapshotDataStart + 'exports[`'.length),
69+
document.positionAt(snapshotDataStart + 'exports[`'.length + name.length),
70+
),
71+
})
72+
}
73+
}
74+
}
75+
76+
export function createSnapshotSymbol(
77+
name: string,
78+
entry: SnapshotEntry,
79+
index: number,
80+
): vscode.DocumentSymbol {
81+
const isLastRound = index === entry.breadcrumb.length - 1
82+
return new vscode.DocumentSymbol(
83+
name,
84+
isLastRound ? 'it' : 'describe',
85+
vscode.SymbolKind.Function,
86+
entry.fullRange,
87+
entry.keyRange,
88+
)
89+
}
90+
91+
export function pushToDocumentSymbol(
92+
parentDocumentSymbol: vscode.DocumentSymbol,
93+
entry: SnapshotEntry,
94+
startIndex: number = 1,
95+
): void {
96+
for (let i = startIndex; i < entry.breadcrumb.length; i++) {
97+
const newDocumentSymbol = createSnapshotSymbol(entry.breadcrumb[i], entry, i)
98+
parentDocumentSymbol.children.push(newDocumentSymbol)
99+
parentDocumentSymbol = newDocumentSymbol
100+
}
101+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
describe('fixture', () => {
4+
describe('__fixtures__/file.spec.ts 1', () => {
5+
it('snapshot', () => {
6+
expect('').toMatchSnapshot()
7+
})
8+
it('snapshot_1', () => {
9+
expect('').toMatchSnapshot()
10+
})
11+
})
12+
13+
describe('__fixtures__/file.spec.ts 2', () => {
14+
it('snapshot_1', () => {
15+
expect('').toMatchSnapshot()
16+
})
17+
it('snapshot_2', () => {
18+
expect('').toMatchSnapshot()
19+
})
20+
it('snapshot', () => {
21+
expect('\nsome content\n').toMatchSnapshot()
22+
})
23+
})
24+
25+
// same name it
26+
describe('__fixtures__/file.spec.ts 2', () => {
27+
it('snapshot_1', () => {
28+
expect('').toMatchSnapshot()
29+
})
30+
it('snapshot_2', () => {
31+
expect('').toMatchSnapshot()
32+
})
33+
it('snapshot', () => {
34+
expect('').toMatchSnapshot()
35+
})
36+
})
37+
// same name expect
38+
it('snapshot_2', () => {
39+
expect('').toMatchSnapshot()
40+
})
41+
})
42+
43+
describe('fixture2', () => {
44+
it('__fixtures__/file.spec.ts 4', () => {
45+
expect('').toMatchSnapshot()
46+
expect('').toMatchSnapshot()
47+
})
48+
})
49+
50+
it('fixture2', () => {
51+
expect('').toMatchSnapshot()
52+
})

test/e2e/utils/downloadSetup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { download } from '@vscode/test-electron'
2-
import type { GlobalSetupContext } from 'vitest/node'
2+
import type { TestProject } from 'vitest/node'
33

4-
export default async function downloadVscode({ provide }: GlobalSetupContext) {
4+
export default async function downloadVscode({ provide }: TestProject) {
55
if (process.env.VSCODE_E2E_DOWNLOAD_PATH)
66
provide('executablePath', process.env.VSCODE_E2E_DOWNLOAD_PATH)
77
else provide('executablePath', await download())

0 commit comments

Comments
 (0)