blob: 46d2de4f308d6d52bcda41884997f4ae01127a9d [file] [log] [blame]
import {
DefinitionProvider,
TextDocument,
CancellationToken,
Definition,
Location,
Position,
ProviderResult,
workspace,
Uri,
} from "vscode";
const NAMED_SNAPSHOT_ASSERTION: RegExp = /(?:\binsta::)?(?:assert(?:_\w+)?_snapshot!)\(\s*['"]([^'"]+)['"]\s*,/;
const STRING_INLINE_SNAPSHOT_ASSERTION: RegExp = /(?:\binsta::)?(?:assert(?:_\w+)?_snapshot!)\(\s*['"]([^'"]+)['"]\s*,\s*@(r#*)?["']/;
const UNNAMED_SNAPSHOT_ASSERTION: RegExp = /(?:\binsta::)?(?:assert(?:_\w+)?_snapshot!)\(/;
const INLINE_MARKER: RegExp = /@(r#*)?["']/;
const FUNCTION: RegExp = /\bfn\s+([\w]+)\s*\(/;
const TEST_DECL: RegExp = /#\[test\]/;
const FILENAME_PARTITION: RegExp = /^(.*)[/\\](.*?)\.rs$/;
const SNAPSHOT_FUNCTION_STRIP: RegExp = /^test_(.*?)$/;
const SNAPSHOT_HEADER: RegExp = /^---\s*$(.*?)^---\s*$/ms;
type SnapshotMatch = {
snapshotName: string | null;
line: number | null;
path: string;
localModuleName: string | null;
snapshotType: "inline" | "named";
};
type ResolvedSnapshotMatch = SnapshotMatch & {
snapshotUri: Uri;
};
export class SnapshotPathProvider implements DefinitionProvider {
/**
* This looks up an explicitly named snapshot (simple case)
*/
private resolveNamedSnapshot(
document: TextDocument,
position: Position
): SnapshotMatch | null {
const line =
(position.line >= 1 ? document.lineAt(position.line - 1).text : "") +
document.lineAt(position.line).text;
const snapshotMatch = line.match(NAMED_SNAPSHOT_ASSERTION);
if (!snapshotMatch) {
return null;
}
const snapshotName = snapshotMatch[1];
const fileNameMatch = document.fileName.match(FILENAME_PARTITION);
if (!fileNameMatch) {
return null;
}
const path = fileNameMatch[1];
const localModuleName = fileNameMatch[2];
return {
snapshotName,
line: null,
path,
localModuleName,
snapshotType: "named",
};
}
/**
* This locates an implicitly (unnamed) snapshot.
*/
private resolveUnnamedSnapshot(
document: TextDocument,
position: Position,
noInline: boolean
): SnapshotMatch | null {
function unnamedSnapshotAt(lineno: number): boolean {
const line = document.lineAt(lineno).text;
return !!(
line.match(UNNAMED_SNAPSHOT_ASSERTION) &&
!line.match(NAMED_SNAPSHOT_ASSERTION) &&
(noInline || !line.match(STRING_INLINE_SNAPSHOT_ASSERTION))
);
}
// if we can't find an unnnamed snapshot at the given position we bail.
if (!unnamedSnapshotAt(position.line)) {
return null;
}
// otherwise scan backwards for unnamed snapshot matches until we find
// a test function declaration.
let snapshotNumber = 1;
let scanLine = position.line - 1;
let functionName = null;
let isInline = !!document.lineAt(position.line).text.match(INLINE_MARKER);
console.log("inline", document.lineAt(position.line), isInline);
while (scanLine >= 0) {
// stop if we find a test function declaration
let functionMatch;
const line = document.lineAt(scanLine);
if (
scanLine > 1 &&
(functionMatch = line.text.match(FUNCTION)) &&
document.lineAt(scanLine - 1).text.match(TEST_DECL)
) {
functionName = functionMatch[1];
break;
}
if (!isInline && line.text.match(INLINE_MARKER)) {
isInline = true;
}
if (unnamedSnapshotAt(scanLine)) {
// TODO: do not increment if the snapshot at that location
snapshotNumber++;
}
scanLine--;
}
// if we couldn't find a function or an unexpected inline snapshot we have to bail.
if (!functionName || (noInline && isInline)) {
return null;
}
let snapshotName = null;
let line = null;
let path = null;
let localModuleName = null;
if (isInline) {
line = position.line;
path = document.fileName;
} else {
snapshotName = `${functionName.match(SNAPSHOT_FUNCTION_STRIP)![1]}${
snapshotNumber > 1 ? `-${snapshotNumber}` : ""
}`;
const fileNameMatch = document.fileName.match(FILENAME_PARTITION);
if (!fileNameMatch) {
return null;
}
path = fileNameMatch[1];
localModuleName = fileNameMatch[2];
}
return {
snapshotName,
line,
path,
localModuleName,
snapshotType: isInline ? "inline" : "named",
};
}
public findSnapshotAtLocation(
document: TextDocument,
position: Position,
token: CancellationToken,
noInline: boolean = false
): Thenable<ResolvedSnapshotMatch | null> {
const snapshotMatch =
this.resolveNamedSnapshot(document, position) ||
this.resolveUnnamedSnapshot(document, position, noInline);
if (!snapshotMatch) {
return Promise.resolve(null);
}
if (snapshotMatch.snapshotType === "inline") {
return Promise.resolve({
snapshotUri: document.uri,
...snapshotMatch,
});
}
const getSearchPath = function (
mode: "exact" | "wildcard-prefix" | "wildcard-all"
): string {
return workspace.asRelativePath(
`${snapshotMatch.path}/snapshots/${mode !== "exact" ? "*__" : ""}${
snapshotMatch.localModuleName
}${mode === "wildcard-all" ? "__*" : ""}__${
snapshotMatch.snapshotName
}.snap`
);
};
function findFiles(path: string): Thenable<Uri | null> {
return workspace
.findFiles(path, "", 1, token)
.then((results) => results[0] || null);
}
// we try to find the file in three passes:
// - exact matching the snapshot folder.
// - with a wildcard module prefix (crate__foo__NAME__SNAP)
// - with a wildcard module prefix and suffix (crate__foo__NAME__tests__SNAP)
// This is needed since snapshots can be contained in submodules. Since
// getting the actual module name is tedious we just hope the match is
// unique.
return findFiles(getSearchPath("exact"))
.then((rv) => rv || findFiles(getSearchPath("wildcard-prefix")))
.then((rv) => rv || findFiles(getSearchPath("wildcard-all")))
.then((snapshot) =>
snapshot ? { snapshotUri: snapshot, ...snapshotMatch } : null
);
}
public provideDefinition(
document: TextDocument,
position: Position,
token: CancellationToken
): ProviderResult<Definition> {
return this.findSnapshotAtLocation(document, position, token, true).then(
(match) => {
if (!match) {
return null;
}
return workspace.fs.readFile(match.snapshotUri).then((contents) => {
const stringContents = Buffer.from(contents).toString("utf-8");
const header = stringContents.match(SNAPSHOT_HEADER);
let location = new Position(0, 0);
if (header) {
location = new Position(header[0].match(/\n/g)!.length + 1, 0);
}
return new Location(match.snapshotUri, location);
});
}
);
}
}