blob: 118418c329443d93314ce5d72a33265c5e2f1969 [file] [log] [blame]
import { platform } from "os";
import {
ExtensionContext,
languages,
workspace,
Uri,
commands,
window,
FileSystemError,
DocumentFilter,
} from "vscode";
import { projectUsesInsta } from "./cargo";
import { InlineSnapshotProvider } from "./InlineSnapshotProvider";
import { processAllSnapshots, processInlineSnapshot } from "./insta";
import { PendingSnapshotsProvider } from "./PendingSnapshotsProvider";
import { Snapshot } from "./Snapshot";
import { SnapshotPathProvider } from "./SnapshotPathProvider";
const INSTA_CONTEXT_NAME = "inInstaSnapshotsProject";
const RUST_FILTER: DocumentFilter = {
scheme: "file",
language: "rust",
};
function getSnapshotPairs(uri: Uri): [Uri, Uri] | undefined {
if (uri.path.match(/\.snap$/)) {
return [uri, Uri.parse(`${uri}.new`)];
} else if (uri.path.match(/\.snap\.new$/)) {
return [uri.with({ path: uri.path.substr(0, uri.path.length - 4) }), uri];
}
}
async function openNamedSnapshotDiff(selectedSnapshot?: Uri) {
if (!selectedSnapshot) {
selectedSnapshot = window.activeTextEditor?.document.uri;
}
if (!selectedSnapshot) {
window.showErrorMessage("No snapshot selected");
return;
}
const pair = getSnapshotPairs(selectedSnapshot);
if (!pair) {
window.showErrorMessage("Not an insta snapshot file");
return;
}
let [oldSnapshot, newSnapshot]: [Uri, Uri] = pair;
try {
await workspace.fs.stat(oldSnapshot);
} catch (e) {
// todo: windows
oldSnapshot = Uri.file(platform() == "win32" ? "NUL" : "/dev/null");
}
await commands.executeCommand(
"vscode.diff",
oldSnapshot,
newSnapshot,
"Snapshot Diff",
{
preview: true,
}
);
}
async function openInlineSnapshotDiff(snapshot: Snapshot) {
const key = encodeURIComponent(snapshot.key);
await commands.executeCommand(
"vscode.diff",
Uri.parse(`instaInlineSnapshot:inline.snap#${key}`),
Uri.parse(`instaInlineSnapshot:inline.snap.new#${key}`),
"Inline Snapshot Diff",
{
preview: true,
}
);
}
async function performSnapshotAction(
action: "accept" | "reject",
pendingSnapshotsProvider: PendingSnapshotsProvider,
selectedSnapshot?: Uri
) {
// in most cases when we're invoked we don't have a selected snapshot yet.
// in that cas we always go by the active text editor's document. However in
// case that document is not a snapshot file (because for instance it's the
// empty file we open for completely new snapshots), then we look at all other
// visible text editors for the first snapshot.
if (!selectedSnapshot) {
selectedSnapshot = window.activeTextEditor?.document.uri;
if (selectedSnapshot && !selectedSnapshot.path.match(/\.snap(\.new)?$/)) {
window.visibleTextEditors.forEach((editor) => {
if (editor.document.uri.path.match(/\.snap(\.new)?$/)) {
selectedSnapshot = editor.document.uri;
}
});
}
}
if (!selectedSnapshot) {
window.showErrorMessage(`Cannot ${action} snapshot: no snapshot selected`);
return;
}
// inline snapshots need to be handled through cargo-insta due to the
// patching. special case it here.
if (selectedSnapshot.scheme === "instaInlineSnapshot") {
const snapshot = pendingSnapshotsProvider.getInlineSnapshot(
selectedSnapshot
);
if (!snapshot || !(await processInlineSnapshot(snapshot, action))) {
window.showErrorMessage(`Cannot ${action} snapshot: cargo-insta failed`);
} else {
const currentActiveUri = window.activeTextEditor?.document.uri;
if (currentActiveUri && selectedSnapshot.path.match(/\.snap(\.new)?$/)) {
commands.executeCommand("workbench.action.closeActiveEditor");
}
}
return;
}
const pair = getSnapshotPairs(selectedSnapshot);
if (!pair) {
window.showErrorMessage(`Cannot ${action} snapshot: not an insta snapshot`);
return;
}
if (action === "accept") {
try {
await workspace.fs.stat(pair[1]);
} catch (error) {
window.showErrorMessage("Could not accept snapshot: no new snapshot");
return;
}
await workspace.fs.rename(pair[1], pair[0], { overwrite: true });
window.showInformationMessage("New snapshot accepted");
} else if (action === "reject") {
try {
await workspace.fs.delete(pair[1]);
} catch (error) {
if (error instanceof FileSystemError && error.code === "FileNotFound") {
window.showInformationMessage("No new snapshot to reject");
} else {
throw error;
}
return;
}
window.showInformationMessage("New snapshot rejected");
}
}
async function switchSnapshotView(selectedSnapshot?: Uri): Promise<void> {
if (!selectedSnapshot) {
selectedSnapshot = window.activeTextEditor?.document.uri;
}
if (!selectedSnapshot) {
return;
}
const pair = getSnapshotPairs(selectedSnapshot);
if (!pair) {
window.showErrorMessage("Not an insta snapshot file");
return;
}
const otherFile = pair[0].path == selectedSnapshot.path ? pair[1] : pair[0];
try {
await workspace.fs.stat(otherFile);
} catch (e) {
window.showInformationMessage("Alternative snapshot does not exist.");
return;
}
await commands.executeCommand("vscode.open", otherFile);
}
async function setInstaContext(value: boolean): Promise<void> {
await commands.executeCommand("setContext", INSTA_CONTEXT_NAME, value);
}
function checkInstaContext() {
const rootUri = workspace.workspaceFolders?.[0].uri;
if (rootUri) {
projectUsesInsta(rootUri).then((usesInsta) => setInstaContext(usesInsta));
} else {
setInstaContext(false);
}
}
function performOnAllSnapshots(op: "accept" | "reject") {
const root = workspace.workspaceFolders?.[0];
if (!root) {
return;
}
processAllSnapshots(root.uri, op).then((okay) => {
if (okay) {
window.showInformationMessage(`Successfully ${op}ed all snapshots.`);
} else {
window.showErrorMessage(`Could not ${op} snapshots.`);
}
});
}
export function activate(context: ExtensionContext): void {
const root = workspace.workspaceFolders?.[0];
const pendingSnapshots = new PendingSnapshotsProvider(root);
const snapshotPathProvider = new SnapshotPathProvider();
const snapWatcher = workspace.createFileSystemWatcher(
"**/*.{snap,snap.new,pending-snap}"
);
snapWatcher.onDidChange(() => pendingSnapshots.refreshDebounced());
snapWatcher.onDidCreate(() => pendingSnapshots.refreshDebounced());
snapWatcher.onDidDelete(() => pendingSnapshots.refreshDebounced());
const cargoTomlWatcher = workspace.createFileSystemWatcher("**/Cargo.toml");
cargoTomlWatcher.onDidChange(() => checkInstaContext());
cargoTomlWatcher.onDidCreate(() => checkInstaContext());
cargoTomlWatcher.onDidDelete(() => checkInstaContext());
if (root) {
projectUsesInsta(root.uri).then((usesInsta) => setInstaContext(usesInsta));
}
context.subscriptions.push(
snapWatcher,
cargoTomlWatcher,
window.registerTreeDataProvider("pendingInstaSnapshots", pendingSnapshots),
workspace.registerTextDocumentContentProvider(
"instaInlineSnapshot",
new InlineSnapshotProvider(pendingSnapshots)
),
languages.registerDefinitionProvider([RUST_FILTER], snapshotPathProvider),
commands.registerCommand(
"mitsuhiko.insta.open-snapshot-diff",
async (selectedFile?: Uri | Snapshot) => {
// when we're invoked from the pending snapshots view the first
// argument is the node (Snapshot) instead of the URI.
if (selectedFile instanceof Snapshot) {
if (selectedFile.inlineInfo) {
await openInlineSnapshotDiff(selectedFile);
return;
} else {
selectedFile = selectedFile.resourceUri;
}
}
await openNamedSnapshotDiff(selectedFile);
}
),
commands.registerCommand(
"mitsuhiko.insta.accept-snapshot",
(selectedFile?: Uri) =>
performSnapshotAction("accept", pendingSnapshots, selectedFile)
),
commands.registerCommand(
"mitsuhiko.insta.reject-snapshot",
(selectedFile?: Uri) =>
performSnapshotAction("reject", pendingSnapshots, selectedFile)
),
commands.registerCommand(
"mitsuhiko.insta.switch-snapshot-view",
(selectedFile?: Uri) => switchSnapshotView(selectedFile)
),
commands.registerCommand("mitsuhiko.insta.refresh-pending-snapshots", () =>
pendingSnapshots.refresh()
),
commands.registerCommand("mitsuhiko.insta.accept-all-snapshots", () =>
performOnAllSnapshots("accept")
),
commands.registerCommand("mitsuhiko.insta.reject-all-snapshots", () =>
performOnAllSnapshots("reject")
)
);
}