diff --git a/README.md b/README.md
index 4125d24b..061a9e89 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,7 @@ A Visual Studio Code extension for [cht.sh](https://cht.sh/).
- ${index} - the index of the snippet (e.g. 2 for the third answer)
- `insertWithDoubleClick`: insert snippet with double click.
- `showCopySuccessNotification`: Whether to show a notification after the snippet is copied to the clipboard.
+- `saveBackups`: Whether to create backups of the snippets.
## Installation
@@ -132,6 +133,41 @@ Saved snippets are displayed in IntelliSense
![Preview](https://raw.githubusercontent.com/mre/vscode-snippet/master/contrib/snippets-storage/search.gif)
+## Restoring snippets from backups
+
+### Restoring with the built-in backup mechanism
+
+vscode-snippet creates backups of your snippets when you delete, rename, move or save snippets. These backups are stored **locally** on your computer.
+
+To restore a backup:
+
+1. Open the Snippets section
+2. Click on the ![History icon](https://raw.githubusercontent.com/mre/vscode-snippet/master/contrib/snippets-storage/history.png) icon (alternatively, you can run the "Restore backups" command)
+3. Select one of the backups from the list
+
+![Demo of restoring backups](https://raw.githubusercontent.com/mre/vscode-snippet/master/contrib/snippets-storage/restore-backups.gif)
+
+### Restoring with the VSCode settings sync
+
+If you have [VSCode settings sync](https://code.visualstudio.com/docs/editor/settings-sync) enabled, you can restore snippets by using VSCode's built-in backup mechanisms: [https://code.visualstudio.com/docs/editor/settings-sync#\_restoring-data](https://code.visualstudio.com/docs/editor/settings-sync#_restoring-data)
+
+## Exporting snippets
+
+VSCode stores snippets in the `state.vscdb` file in a `JSON` format.
+
+To export the snippets:
+
+1. Find the `state.vscdb` file
+ - On Ubuntu Linux: `~/.config/Code/User/globalStorage/state.vscdb`
+ - On Windows: `AppData\Roaming\Code\User\globalStorage\state.vscdb`
+ - On macOS: `~/Library/Application Support/Code/User/globalStorage/state.vscdb`
+2. Inspect the content of this file using some tool that can open SQLite files, for example: [https://inloop.github.io/sqlite-viewer](https://inloop.github.io/sqlite-viewer)
+ 1. On this website, upload the `state.vscdb` file and run the following command:
+ ```sql
+ SELECT * FROM 'ItemTable' WHERE key like 'vscode-snippet.snippet'
+ ```
+ ![SQLite Viewer](https://raw.githubusercontent.com/mre/vscode-snippet/master/contrib/snippets-storage/vscdb.png) 2. Then click "Execute". You should get a single row with the key `vscode-snippet.snippet` and a `JSON` value. This `JSON` contains all of your snippets.
+
## Contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md)
diff --git a/assets/icons/history-dark.svg b/assets/icons/history-dark.svg
new file mode 100644
index 00000000..d82fe00e
--- /dev/null
+++ b/assets/icons/history-dark.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/history-light.svg b/assets/icons/history-light.svg
new file mode 100644
index 00000000..0aded818
--- /dev/null
+++ b/assets/icons/history-light.svg
@@ -0,0 +1,3 @@
+
diff --git a/contrib/snippets-storage/history.png b/contrib/snippets-storage/history.png
new file mode 100644
index 00000000..3b8af266
Binary files /dev/null and b/contrib/snippets-storage/history.png differ
diff --git a/contrib/snippets-storage/restore-backups.gif b/contrib/snippets-storage/restore-backups.gif
new file mode 100644
index 00000000..71b4587a
Binary files /dev/null and b/contrib/snippets-storage/restore-backups.gif differ
diff --git a/contrib/snippets-storage/vscdb.png b/contrib/snippets-storage/vscdb.png
new file mode 100644
index 00000000..49e913c0
Binary files /dev/null and b/contrib/snippets-storage/vscdb.png differ
diff --git a/package-lock.json b/package-lock.json
index 2848f3e6..38203d91 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "snippet",
- "version": "1.1.5",
+ "version": "1.1.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "snippet",
- "version": "1.1.5",
+ "version": "1.1.6",
"license": "MIT",
"dependencies": {
"@vscode/vsce": "^2.24.0",
diff --git a/package.json b/package.json
index d2c2d36e..c3fa9929 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "snippet",
"displayName": "Snippet",
"description": "Insert a snippet from cht.sh for Python, JavaScript, Ruby, C#, Go, Rust (and any other language)",
- "version": "1.1.5",
+ "version": "1.1.6",
"publisher": "vscode-snippet",
"engines": {
"vscode": "^1.74.0"
@@ -123,6 +123,15 @@
"light": "assets/icons/add-light.svg",
"dark": "assets/icons/add-dark.svg"
}
+ },
+ {
+ "title": "Restore backups",
+ "command": "snippet.restoreBackups",
+ "category": "Snippet",
+ "icon": {
+ "light": "assets/icons/history-light.svg",
+ "dark": "assets/icons/history-dark.svg"
+ }
}
],
"configuration": {
@@ -162,6 +171,11 @@
"type": "boolean",
"default": true,
"description": "Whether to show a notification after the snippet is copied to the clipboard."
+ },
+ "snippet.saveBackups": {
+ "type": "boolean",
+ "default": true,
+ "description": "Whether to create backups of the snippets."
}
}
},
@@ -171,6 +185,11 @@
"command": "snippet.createFolder",
"when": "view == snippetsView",
"group": "navigation"
+ },
+ {
+ "command": "snippet.restoreBackups",
+ "when": "view == snippetsView",
+ "group": "navigation"
}
],
"view/item/context": [
diff --git a/src/backupManager.ts b/src/backupManager.ts
new file mode 100644
index 00000000..69679525
--- /dev/null
+++ b/src/backupManager.ts
@@ -0,0 +1,119 @@
+import { randomUUID } from "crypto";
+import * as vscode from "vscode";
+import { getConfig } from "./config";
+import { formatUnixTime } from "./date";
+import SnippetsStorage, {
+ StorageOperation,
+ TreeElement,
+} from "./snippetsStorage";
+
+export interface Backup {
+ id: string;
+ dateUnix: number;
+ elements: TreeElement[];
+ beforeOperation?: string;
+}
+
+export interface BackupItem extends vscode.QuickPickItem {
+ item: Backup;
+}
+
+const STORAGE_KEY = "snippet.snippetBackupsStorageKey";
+const MAX_BACKUPS = 10;
+
+export class BackupManager {
+ private backups: Backup[] = [];
+ private elementsBeforeRestore: TreeElement[] | null = null;
+
+ constructor(
+ private readonly context: vscode.ExtensionContext,
+ private readonly snippets: SnippetsStorage
+ ) {
+ this.load();
+ snippets.onBeforeSave = (elements, operation) =>
+ this.makeBackup(elements, operation);
+ }
+
+ getBackupItems(): BackupItem[] {
+ const items = this.backups.map((backup) => {
+ const time = `${formatUnixTime(backup.dateUnix)}`;
+ const detail = backup.beforeOperation
+ ? `before "${backup.beforeOperation}"`
+ : undefined;
+ const description = `${this.snippets.getSnippetCount(
+ backup.elements
+ )} snippet${
+ this.snippets.getSnippetCount(backup.elements) === 1 ? "" : "s"
+ }`;
+
+ return {
+ label: time,
+ item: backup,
+ description,
+ detail,
+ };
+ });
+
+ items.sort((a, b) => b.item.dateUnix - a.item.dateUnix);
+
+ return items;
+ }
+
+ async restoreBackup(id: string) {
+ const backup = this.backups.find((backup) => backup.id === id);
+
+ if (!backup) {
+ console.error(`Backup with id ${id} not found.`);
+ return;
+ }
+
+ this.elementsBeforeRestore = this.snippets.getElements();
+ await this.snippets.replaceElements(backup.elements);
+ }
+
+ async undoLastRestore() {
+ if (this.elementsBeforeRestore === null) {
+ return;
+ }
+
+ await this.snippets.replaceElements(this.elementsBeforeRestore);
+ this.elementsBeforeRestore = null;
+ }
+
+ private load(): void {
+ this.backups = JSON.parse(
+ this.context.globalState.get(STORAGE_KEY) || "[]"
+ ) as Backup[];
+ }
+
+ private async makeBackup(
+ elements: TreeElement[],
+ operation?: StorageOperation
+ ) {
+ if (!getConfig("saveBackups")) {
+ return;
+ }
+
+ const backup: Backup = {
+ id: randomUUID(),
+ dateUnix: Date.now(),
+ elements,
+ beforeOperation: operation,
+ };
+
+ this.backups.push(backup);
+
+ if (this.backups.length > MAX_BACKUPS) {
+ this.backups.shift();
+ }
+
+ await this.save();
+ }
+
+ private async save() {
+ await this.context.globalState.update(
+ STORAGE_KEY,
+ JSON.stringify(this.backups)
+ );
+ }
+}
diff --git a/src/config.ts b/src/config.ts
index 38693467..2fe6e25e 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -17,8 +17,6 @@ export async function pickLanguage() {
const languages = await vscode.languages.getLanguages();
const disposables: Disposable[] = [];
- // return await vscode.window.showQuickPick(languages);
-
try {
return await new Promise((resolve) => {
const input = vscode.window.createQuickPick();
diff --git a/src/date.ts b/src/date.ts
new file mode 100644
index 00000000..9038f34c
--- /dev/null
+++ b/src/date.ts
@@ -0,0 +1,7 @@
+export function formatUnixTime(ms: number): string {
+ const date = new Date(ms);
+ return `${date.toDateString()}, ${date.toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ })}`;
+}
diff --git a/src/endpoints.ts b/src/endpoints.ts
index aa304da0..9e9b4f6f 100644
--- a/src/endpoints.ts
+++ b/src/endpoints.ts
@@ -1,12 +1,14 @@
import * as vscode from "vscode";
+import { BackupManager } from "./backupManager";
import * as clipboard from "./clipboard";
-import { pickLanguage, getLanguage, getConfig } from "./config";
-import { query } from "./query";
+import { getConfig, getLanguage, pickLanguage } from "./config";
+import { formatUnixTime } from "./date";
+import languages from "./languages";
import { encodeRequest } from "./provider";
+import { query } from "./query";
import snippet from "./snippet";
-import { SnippetsTreeProvider, SnippetsTreeItem } from "./snippetsTreeProvider";
import SnippetsStorage from "./snippetsStorage";
-import languages from "./languages";
+import { SnippetsTreeItem, SnippetsTreeProvider } from "./snippetsTreeProvider";
export interface Request {
language: string;
@@ -383,3 +385,30 @@ export function createFolder(treeProvider: SnippetsTreeProvider) {
await treeProvider.storage.createFolder(folderName, item?.id);
};
}
+
+export function showBackups(backupManager: BackupManager) {
+ return async () => {
+ const backups = backupManager.getBackupItems();
+ const selectedBackup = await vscode.window.showQuickPick(backups, {
+ placeHolder:
+ "Select a backup to restore. You will be able to undo this operation.",
+ title: "Select a backup",
+ });
+
+ if (!selectedBackup) {
+ return;
+ }
+
+ await backupManager.restoreBackup(selectedBackup.item.id);
+ await vscode.commands.executeCommand("snippetsView.focus");
+ const answer = await vscode.window.showInformationMessage(
+ `Restored backup from ${formatUnixTime(selectedBackup.item.dateUnix)}`,
+ "Ok",
+ "Undo"
+ );
+
+ if (answer === "Undo") {
+ await backupManager.undoLastRestore();
+ }
+ };
+}
diff --git a/src/extension.ts b/src/extension.ts
index b30c116e..109a954d 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -1,12 +1,13 @@
"use strict";
import * as vscode from "vscode";
+import { BackupManager } from "./backupManager";
import { cache } from "./cache";
-import SnippetProvider from "./provider";
+import { CompletionManager } from "./completionManager";
import * as endpoints from "./endpoints";
-import { SnippetsTreeProvider } from "./snippetsTreeProvider";
+import SnippetProvider from "./provider";
import SnippetsStorage from "./snippetsStorage";
-import { CompletionManager } from "./completionManager";
+import { SnippetsTreeProvider } from "./snippetsTreeProvider";
export function activate(ctx: vscode.ExtensionContext) {
const snippetStorageKey = "snippet.snippetsStorageKey";
@@ -15,66 +16,114 @@ export function activate(ctx: vscode.ExtensionContext) {
const snippetsStorage = new SnippetsStorage(ctx, snippetStorageKey);
const snippetsTreeProvider = new SnippetsTreeProvider(ctx, snippetsStorage);
new CompletionManager(ctx, snippetsStorage);
+ const backupManager = new BackupManager(ctx, snippetsStorage);
- vscode.commands.registerCommand("snippet.find", () =>
- endpoints.findDefault(snippetsStorage)
+ ctx.subscriptions.push(
+ vscode.commands.registerCommand("snippet.find", () =>
+ endpoints.findDefault(snippetsStorage)
+ )
);
- vscode.commands.registerCommand("snippet.findForLanguage", () =>
- endpoints.findForLanguage(snippetsStorage)
+ ctx.subscriptions.push(
+ vscode.commands.registerCommand("snippet.findForLanguage", () =>
+ endpoints.findForLanguage(snippetsStorage)
+ )
);
- vscode.commands.registerCommand("snippet.findInplace", () =>
- endpoints.findInplace(snippetsStorage)
+ ctx.subscriptions.push(
+ vscode.commands.registerCommand("snippet.findInplace", () =>
+ endpoints.findInplace(snippetsStorage)
+ )
);
- vscode.commands.registerCommand("snippet.findInNewEditor", () =>
- endpoints.findInNewEditor(snippetsStorage)
+ ctx.subscriptions.push(
+ vscode.commands.registerCommand("snippet.findInNewEditor", () =>
+ endpoints.findInNewEditor(snippetsStorage)
+ )
);
- vscode.commands.registerCommand(
- "snippet.findSelectedText",
- endpoints.findSelectedText
+ ctx.subscriptions.push(
+ vscode.commands.registerCommand(
+ "snippet.findSelectedText",
+ endpoints.findSelectedText
+ )
);
- vscode.commands.registerCommand("snippet.showPreviousAnswer", () =>
- endpoints.showPreviousAnswer(snippetsStorage)
+ ctx.subscriptions.push(
+ vscode.commands.registerCommand("snippet.showPreviousAnswer", () =>
+ endpoints.showPreviousAnswer(snippetsStorage)
+ )
);
- vscode.commands.registerCommand("snippet.showNextAnswer", () =>
- endpoints.showNextAnswer(snippetsStorage)
+ ctx.subscriptions.push(
+ vscode.commands.registerCommand("snippet.showNextAnswer", () =>
+ endpoints.showNextAnswer(snippetsStorage)
+ )
);
- vscode.commands.registerCommand(
- "snippet.toggleComments",
- endpoints.toggleComments
+ ctx.subscriptions.push(
+ vscode.commands.registerCommand(
+ "snippet.toggleComments",
+ endpoints.toggleComments
+ )
);
- vscode.commands.registerCommand(
- "snippet.saveSnippet",
- endpoints.saveSnippet(snippetsTreeProvider)
+ ctx.subscriptions.push(
+ vscode.commands.registerCommand(
+ "snippet.saveSnippet",
+ endpoints.saveSnippet(snippetsTreeProvider)
+ )
+ );
+ ctx.subscriptions.push(
+ vscode.commands.registerCommand(
+ "snippet.insertSnippet",
+ endpoints.insertSnippet(snippetsTreeProvider)
+ )
);
- vscode.commands.registerCommand(
- "snippet.insertSnippet",
- endpoints.insertSnippet(snippetsTreeProvider)
+ ctx.subscriptions.push(
+ vscode.commands.registerCommand(
+ "snippet.deleteSnippet",
+ endpoints.deleteSnippet(snippetsTreeProvider)
+ )
);
- vscode.commands.registerCommand(
- "snippet.deleteSnippet",
- endpoints.deleteSnippet(snippetsTreeProvider)
+ ctx.subscriptions.push(
+ vscode.commands.registerCommand(
+ "snippet.renameSnippet",
+ endpoints.renameSnippet(snippetsTreeProvider)
+ )
);
- vscode.commands.registerCommand(
- "snippet.renameSnippet",
- endpoints.renameSnippet(snippetsTreeProvider)
+ ctx.subscriptions.push(
+ vscode.commands.registerCommand(
+ "snippet.copySnippet",
+ endpoints.copySnippet(snippetsTreeProvider)
+ )
);
- vscode.commands.registerCommand(
- "snippet.copySnippet",
- endpoints.copySnippet(snippetsTreeProvider)
+ ctx.subscriptions.push(
+ vscode.commands.registerCommand(
+ "snippet.findAndCopy",
+ endpoints.findAndCopy(snippetsStorage)
+ )
);
- vscode.commands.registerCommand(
- "snippet.findAndCopy",
- endpoints.findAndCopy(snippetsStorage)
+ ctx.subscriptions.push(
+ vscode.commands.registerCommand(
+ "snippet.createFolder",
+ endpoints.createFolder(snippetsTreeProvider)
+ )
);
- vscode.commands.registerCommand(
- "snippet.createFolder",
- endpoints.createFolder(snippetsTreeProvider)
+ ctx.subscriptions.push(
+ vscode.commands.registerCommand(
+ "snippet.restoreBackups",
+ endpoints.showBackups(backupManager)
+ )
);
+ if (process.env.NODE_ENV === "test") {
+ // Expose the "moveElement" method for test purposes
+ ctx.subscriptions.push(
+ vscode.commands.registerCommand(
+ "snippet.test_moveElement",
+ async (sourceId, targetId) =>
+ await snippetsStorage.moveElement(sourceId, targetId)
+ )
+ );
+ }
+
cache.state = ctx.globalState;
const provider = new SnippetProvider();
- const disposableProvider =
- vscode.workspace.registerTextDocumentContentProvider("snippet", provider);
- ctx.subscriptions.push(disposableProvider);
+ ctx.subscriptions.push(
+ vscode.workspace.registerTextDocumentContentProvider("snippet", provider)
+ );
}
diff --git a/src/snippetsStorage.ts b/src/snippetsStorage.ts
index 6851d0d9..52167bfe 100644
--- a/src/snippetsStorage.ts
+++ b/src/snippetsStorage.ts
@@ -19,8 +19,21 @@ export interface FolderListItem extends vscode.QuickPickItem {
label: string;
}
+export enum StorageOperation {
+ Save = "save snippet",
+ LoadDefault = "load default snippets",
+ Delete = "delete",
+ Rename = "rename",
+ CreateFolder = "create folder",
+ Move = "move",
+}
+
export default class SnippetsStorage {
public onSave?: () => void;
+ public onBeforeSave?: (
+ elements: TreeElement[],
+ operation?: StorageOperation
+ ) => void;
public onSnippetSave?: (snippet: TreeElement) => void;
private readonly elements = new Map();
private rootId = "";
@@ -36,6 +49,10 @@ export default class SnippetsStorage {
}
}
+ getElements(): TreeElement[] {
+ return [...this.elements.values()];
+ }
+
getFoldersList(): FolderListItem[] {
const result: FolderListItem[] = [];
@@ -58,7 +75,7 @@ export default class SnippetsStorage {
for (const childId of parent.childIds) {
const child = this.getElement(childId);
- if (this.isFolder(child)) {
+ if (SnippetsStorage.isFolder(child)) {
const joinedName = `${
current.label === "/" ? "/" : `${current.label}/`
}${child.data.label}`;
@@ -81,7 +98,7 @@ export default class SnippetsStorage {
async deleteElement(id: string): Promise {
const toDelete = this.getElement(id);
- const messageForUser = this.isFolder(toDelete)
+ const messageForUser = SnippetsStorage.isFolder(toDelete)
? "Are you sure you want to delete this folder? Everything inside it will be deleted too."
: "Are you sure you want to delete this snippet?";
@@ -103,18 +120,18 @@ export default class SnippetsStorage {
1
);
- await this.save();
+ await this.save(StorageOperation.Delete);
}
async renameElement(id: string, newName: string): Promise {
this.getElement(id).data.label = newName;
- await this.save();
+ await this.save(StorageOperation.Rename);
}
async createFolder(name: string, relativeToId?: string): Promise {
const relativeToElement = this.getElement(relativeToId);
- const parentId = this.isFolder(relativeToElement)
+ const parentId = SnippetsStorage.isFolder(relativeToElement)
? relativeToElement.data.id
: relativeToElement.parentId;
@@ -127,7 +144,7 @@ export default class SnippetsStorage {
this.elements.set(folder.id, { childIds: [], data: folder, parentId });
this.getElement(parentId).childIds?.push(folder.id);
- await this.save();
+ await this.save(StorageOperation.CreateFolder);
}
async moveElement(sourceId: string, targetId?: string): Promise {
@@ -138,7 +155,7 @@ export default class SnippetsStorage {
const sourceElement = this.getElement(sourceId);
const targetElement = this.getElement(targetId);
- const newParentId = this.isFolder(targetElement)
+ const newParentId = SnippetsStorage.isFolder(targetElement)
? targetElement.data.id
: targetElement.parentId;
@@ -164,7 +181,7 @@ export default class SnippetsStorage {
const newParentElement = this.getElement(newParentId);
newParentElement.childIds?.push(sourceId);
- await this.save();
+ await this.save(StorageOperation.Move);
}
async saveSnippet(
@@ -184,7 +201,7 @@ export default class SnippetsStorage {
this.elements.set(data.id, element);
this.getElement(parentId).childIds?.push(data.id);
- await this.save();
+ await this.save(StorageOperation.Save);
this.onSnippetSave?.(element);
}
@@ -192,23 +209,37 @@ export default class SnippetsStorage {
return this.getElement(id).data.content?.toString() || "";
}
- async save(): Promise {
+ async save(operation?: StorageOperation): Promise {
+ const originalElements = JSON.parse(
+ this.context.globalState.get(this.storageKey) || "[]"
+ ) as TreeElement[];
+ this.onBeforeSave?.(originalElements, operation);
await this.context.globalState.update(this.storageKey, this.serialize());
this.onSave?.();
}
- load(): void {
- this.deserialize(this.context.globalState.get(this.storageKey) || "[]");
- }
-
*getSnippets(): IterableIterator {
for (const element of this.elements.values()) {
- if (!this.isFolder(element)) {
+ if (!SnippetsStorage.isFolder(element)) {
yield element;
}
}
}
+ getSnippetCount(elements: TreeElement[]) {
+ return elements.filter((x) => !SnippetsStorage.isFolder(x)).length;
+ }
+
+ async replaceElements(newElements: TreeElement[]): Promise {
+ this.deserialize(JSON.stringify(newElements));
+ await this.context.globalState.update(this.storageKey, this.serialize());
+ this.onSave?.();
+ }
+
+ private load(): void {
+ this.deserialize(this.context.globalState.get(this.storageKey) || "[]");
+ }
+
private async loadDefaultElements(): Promise {
const root: TreeElementData = {
id: randomUUID(),
@@ -246,7 +277,7 @@ export default class SnippetsStorage {
parentId: exampleFolder.id,
});
- await this.save();
+ await this.save(StorageOperation.LoadDefault);
}
private serialize(): string {
@@ -266,7 +297,7 @@ export default class SnippetsStorage {
});
}
- private isFolder(element: TreeElement): boolean {
+ static isFolder(element: TreeElement): boolean {
return element.childIds != null;
}
}
diff --git a/src/test/suite/findSelectedText.test.ts b/src/test/suite/findSelectedText.test.ts
index 802be822..68c3c5c1 100644
--- a/src/test/suite/findSelectedText.test.ts
+++ b/src/test/suite/findSelectedText.test.ts
@@ -1,6 +1,6 @@
import * as assert from "assert";
-import * as vscode from "vscode";
import { after, before } from "mocha";
+import * as vscode from "vscode";
import {
closeAllEditors,
getInitialDocument,
diff --git a/src/test/suite/restoreBackups.test.ts b/src/test/suite/restoreBackups.test.ts
new file mode 100644
index 00000000..587b8fb2
--- /dev/null
+++ b/src/test/suite/restoreBackups.test.ts
@@ -0,0 +1,188 @@
+import * as assert from "assert";
+import { afterEach } from "mocha";
+import * as sinon from "sinon";
+import * as vscode from "vscode";
+import { MessageItem } from "vscode";
+import { BackupItem } from "../../backupManager";
+import { cache } from "../../cache";
+import SnippetsStorage, { TreeElement } from "../../snippetsStorage";
+import { closeAllEditors, openDocumentAndSelectText } from "../testUtils";
+
+suite("snippet.restoreBackups", () => {
+ afterEach(() => {
+ sinon.restore();
+ });
+
+ test("No backups initially", async () => {
+ const backups = await getBackups();
+
+ assert.strictEqual(backups.length, 0);
+ });
+
+ test("Creates a backup after rename", async () => {
+ sinon.stub(vscode.window, "showInputBox").callsFake(() => {
+ return Promise.resolve("new name");
+ });
+ const originalElementsJson = getElementsJson();
+ const snippet = getAnySnippet(originalElementsJson);
+
+ await vscode.commands.executeCommand("snippet.renameSnippet", {
+ id: snippet.data.id,
+ });
+ const backups = await getBackups();
+
+ assert.strictEqual(backups.length, 1);
+ assert.strictEqual(
+ JSON.stringify(backups[0].item.elements),
+ originalElementsJson
+ );
+ });
+
+ test("Creates a backup after delete", async () => {
+ sinon.stub(vscode.window, "showInformationMessage").callsFake(() => {
+ return Promise.resolve("Yes" as unknown as MessageItem);
+ });
+ const originalElementsJson = getElementsJson();
+ const snippet = getAnySnippet(originalElementsJson);
+
+ await vscode.commands.executeCommand("snippet.deleteSnippet", {
+ id: snippet.data.id,
+ });
+ const backups = await getBackups();
+
+ assert.strictEqual(backups.length, 2);
+ assert.strictEqual(
+ JSON.stringify(backups[0].item.elements),
+ originalElementsJson
+ );
+ });
+
+ test("Creates a backup after save", async () => {
+ await openDocumentAndSelectText({
+ language: "javascript",
+ queryText: "query",
+ openInNewEditor: true,
+ });
+ sinon.stub(vscode.window, "showQuickPick").callsFake((folders) => {
+ return folders[0];
+ });
+ sinon.stub(vscode.window, "showInputBox").callsFake(() => {
+ return Promise.resolve("new snippet");
+ });
+ const originalElementsJson = getElementsJson();
+
+ await vscode.commands.executeCommand("snippet.saveSnippet");
+ sinon.restore();
+ const backups = await getBackups();
+ await closeAllEditors();
+
+ assert.strictEqual(backups.length, 3);
+ assert.strictEqual(
+ JSON.stringify(backups[0].item.elements),
+ originalElementsJson
+ );
+ });
+
+ test("Creates a backup after move", async () => {
+ const originalElementsJson = getElementsJson();
+ const elements = parseElements(originalElementsJson);
+
+ await vscode.commands.executeCommand(
+ "snippet.test_moveElement",
+ elements[2].data.id,
+ elements[1].data.id
+ );
+ const backups = await getBackups();
+
+ assert.strictEqual(backups.length, 4);
+ assert.strictEqual(
+ JSON.stringify(backups[0].item.elements),
+ originalElementsJson
+ );
+ });
+
+ test("Restores backup", async () => {
+ sinon.stub(vscode.window, "showInputBox").callsFake(() => {
+ return Promise.resolve("new name");
+ });
+ sinon.stub(vscode.window, "showInformationMessage").callsFake(() => {
+ return Promise.resolve("Ok" as unknown as MessageItem);
+ });
+ const originalElementsJson = getElementsJson();
+ const snippet = getAnySnippet(originalElementsJson);
+
+ await vscode.commands.executeCommand("snippet.renameSnippet", {
+ id: snippet.data.id,
+ });
+ const backups = await getBackups(true);
+
+ assert.strictEqual(backups.length, 5);
+ assert.strictEqual(getElementsJson(), originalElementsJson);
+ });
+
+ test("Undoes backup restoration", async () => {
+ sinon.stub(vscode.window, "showInformationMessage").callsFake(() => {
+ return Promise.resolve("Undo" as unknown as MessageItem);
+ });
+ sinon.stub(vscode.window, "showInputBox").callsFake(() => {
+ return Promise.resolve("new name");
+ });
+ const originalElementsJson = getElementsJson();
+ const snippet = getAnySnippet(originalElementsJson);
+
+ await vscode.commands.executeCommand("snippet.renameSnippet", {
+ id: snippet.data.id,
+ });
+ const elementsAfterChange = getElementsJson();
+ const backups = await getBackups(true);
+
+ assert.strictEqual(backups.length, 6);
+ assert.notStrictEqual(getElementsJson(), originalElementsJson);
+ assert.strictEqual(getElementsJson(), elementsAfterChange);
+ });
+
+ test("Saves up to 10 backups", async () => {
+ sinon.stub(vscode.window, "showInputBox").callsFake(() => {
+ return Promise.resolve("new name");
+ });
+ const originalElementsJson = getElementsJson();
+ const snippet = getAnySnippet(originalElementsJson);
+
+ for (let i = 0; i < 11; i++) {
+ await vscode.commands.executeCommand("snippet.renameSnippet", {
+ id: snippet.data.id,
+ });
+ }
+ const backups = await getBackups();
+
+ assert.strictEqual(backups.length, 10);
+ });
+});
+
+async function getBackups(restoreLatest = false): Promise {
+ return new Promise((resolve) => {
+ const showQuickPickStub = sinon.stub(vscode.window, "showQuickPick");
+
+ let result: BackupItem[] = [];
+ showQuickPickStub.callsFake(async (backups: BackupItem[]) => {
+ result = backups;
+ return restoreLatest && backups[0] ? backups[0] : null;
+ });
+
+ vscode.commands.executeCommand("snippet.restoreBackups").then(() => {
+ resolve(result);
+ });
+ });
+}
+
+function getElementsJson(): string {
+ return cache.state.get("snippet.snippetsStorageKey") || "[]";
+}
+
+function parseElements(json: string): TreeElement[] {
+ return JSON.parse(json);
+}
+
+function getAnySnippet(elementsJson: string): TreeElement {
+ return parseElements(elementsJson).find((x) => !SnippetsStorage.isFolder(x));
+}
diff --git a/src/test/testUtils.ts b/src/test/testUtils.ts
index 565f3599..802ce125 100644
--- a/src/test/testUtils.ts
+++ b/src/test/testUtils.ts
@@ -1,6 +1,6 @@
+import * as sinon from "sinon";
import * as vscode from "vscode";
import { MockResponseData } from "../snippet";
-import * as sinon from "sinon";
export function getResponseFromResultDocument(): MockResponseData {
const editors = vscode.window.visibleTextEditors.filter(
@@ -34,7 +34,7 @@ export async function openDocument({
await config.update("openInNewEditor", openInNewEditor, configTarget);
}
-export async function openDocumentAndFindSelectedText({
+export async function openDocumentAndSelectText({
language = "javascript",
queryText = Date.now().toString(),
openInNewEditor,
@@ -51,6 +51,22 @@ export async function openDocumentAndFindSelectedText({
0,
queryText.length
);
+}
+
+export async function openDocumentAndFindSelectedText({
+ language = "javascript",
+ queryText = Date.now().toString(),
+ openInNewEditor,
+}: {
+ language?: string;
+ queryText?: string;
+ openInNewEditor: boolean;
+}): Promise {
+ await openDocumentAndSelectText({
+ language,
+ queryText,
+ openInNewEditor,
+ });
await vscode.commands.executeCommand("snippet.findSelectedText");
}