Skip to content

Commit

Permalink
Add file selection history feature
Browse files Browse the repository at this point in the history
  • Loading branch information
Thomas authored and Thomas committed Oct 11, 2024
1 parent a589351 commit 75edf43
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 25 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Copy file contents in XML format for LLM prompts effortlessly.
- **Custom System Message**: Optionally include a system message in your copied output, encapsulated within a `<systemMessage>` XML element.
- **Configurable Shortcuts**: Quickly refresh the file tree or copy files using customizable keyboard shortcuts.
- **Git Ignore Support**: Automatically ignores files and directories specified in your .gitignore.
- **Selection History**: Quickly switch between sets of previously selected files.

## Installation

Expand Down
30 changes: 28 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "Files2Prompt",
"icon": "./files2prompt-icon.webp",
"description": "Copy file contents for LLM prompts",
"version": "1.2.0",
"version": "1.3.0",
"publisher": "thomas-mckanna",
"keywords": [
"files",
Expand All @@ -23,7 +23,7 @@
"Other"
],
"activationEvents": [
"onView.files2prompView"
"onView.files2PromptView"
],
"main": "./out/extension.js",
"contributes": {
Expand Down Expand Up @@ -68,6 +68,22 @@
"light": "resources/light/clear.svg",
"dark": "resources/dark/clear.svg"
}
},
{
"command": "files2prompt.goBack",
"title": "Go Back",
"icon": {
"light": "resources/light/back.svg",
"dark": "resources/dark/back.svg"
}
},
{
"command": "files2prompt.goForward",
"title": "Go Forward",
"icon": {
"light": "resources/light/forward.svg",
"dark": "resources/dark/forward.svg"
}
}
],
"menus": {
Expand All @@ -86,6 +102,16 @@
"command": "files2prompt.clearChecks",
"when": "view == files2PromptView",
"group": "navigation@3"
},
{
"command": "files2prompt.goBack",
"when": "view == files2PromptView",
"group": "navigation@4"
},
{
"command": "files2prompt.goForward",
"when": "view == files2PromptView",
"group": "navigation@5"
}
]
},
Expand Down
16 changes: 16 additions & 0 deletions resources/dark/back.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions resources/dark/forward.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions resources/light/back.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions resources/light/forward.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 54 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import * as vscode from "vscode";
import * as fs from "fs";
import * as path from "path";
import { FileTreeProvider, FileItem } from "./fileTreeProvider";
import { FileTreeProvider } from "./fileTreeProvider";

export function activate(context: vscode.ExtensionContext) {
const workspaceFolders = vscode.workspace.workspaceFolders;
Expand All @@ -17,6 +17,9 @@ export function activate(context: vscode.ExtensionContext) {
manageCheckboxStateManually: true,
});

let history: string[][] = [];
let historyPosition: number = -1;

context.subscriptions.push(
vscode.commands.registerCommand("files2prompt.refresh", () =>
fileTreeProvider.refresh()
Expand All @@ -29,6 +32,17 @@ export function activate(context: vscode.ExtensionContext) {
return;
}

// Before saving the current selection to history, check if it's the same as the last selection
const lastSelection = history[historyPosition] || [];
if (!arraysEqual(checkedFiles, lastSelection)) {
// Save the current selection to the history
if (historyPosition < history.length - 1) {
history = history.slice(0, historyPosition + 1);
}
history.push([...checkedFiles]); // Save a copy of the current selection
historyPosition++;
}

const xmlOutput = await generateXmlOutput(checkedFiles);

// Include system message if provided
Expand All @@ -53,8 +67,35 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand("files2prompt.clearChecks", () => {
fileTreeProvider.clearChecks();
vscode.window.showInformationMessage("All checks have been cleared.");
}),
vscode.commands.registerCommand("files2prompt.goBack", async () => {
if (historyPosition > 0) {
historyPosition--;
const previousSelection = history[historyPosition];

// Update the file selections in the FileTreeProvider
await fileTreeProvider.setCheckedFiles(previousSelection);
} else {
// Show warning message
vscode.window.showWarningMessage(
"No previous selection to go back to."
);
}
}),
vscode.commands.registerCommand("files2prompt.goForward", async () => {
if (historyPosition < history.length - 1) {
historyPosition++;
const nextSelection = history[historyPosition];

// Update the file selections in the FileTreeProvider
await fileTreeProvider.setCheckedFiles(nextSelection);
} else {
// Show warning message
vscode.window.showWarningMessage(
"No next selection to go forward to."
);
}
})
// Add keybindings if necessary (optional)
);

// Handle checkbox state changes asynchronously
Expand Down Expand Up @@ -97,3 +138,14 @@ async function generateXmlOutput(filePaths: string[]): Promise<string> {

return `<files>\n${xmlContent}</files>`;
}

// Helper function to compare arrays of strings
function arraysEqual(a: string[], b: string[]): boolean {
if (a.length !== b.length) return false;
const sortedA = [...a].sort();
const sortedB = [...b].sort();
for (let i = 0; i < sortedA.length; i++) {
if (sortedA[i] !== sortedB[i]) return false;
}
return true;
}
73 changes: 52 additions & 21 deletions src/fileTreeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export class FileTreeProvider implements vscode.TreeDataProvider<FileItem> {
}

getTreeItem(element: FileItem): vscode.TreeItem {
const key = element.resourceUri.fsPath;
const checkboxState =
this.checkedItems.get(key) ?? vscode.TreeItemCheckboxState.Unchecked;
element.checkboxState = checkboxState;
return element;
}

Expand Down Expand Up @@ -95,37 +99,40 @@ export class FileTreeProvider implements vscode.TreeDataProvider<FileItem> {

if (item.isDirectory) {
await this.updateDirectoryCheckState(key, state);
} else {
// If it's a file, update its parent directory's state
const parentDir = path.dirname(key);
await this.updateParentState(parentDir);
}

// Update parent directories' states
let dirPath = path.dirname(key);
while (dirPath.startsWith(this.workspaceRoot)) {
await this.updateParentState(dirPath);
dirPath = path.dirname(dirPath);
}

this.refresh();
}

// Make updateParentState async
private async updateParentState(dirPath: string): Promise<void> {
const parentKey = path.dirname(dirPath);
const siblings = await fs.promises.readdir(dirPath);

const allChecked = await Promise.all(
siblings.map(async (sibling) => {
const siblingPath = path.join(dirPath, sibling);
const isIgnored = this.isGitIgnored(
path.relative(this.workspaceRoot, siblingPath)
);
if (isIgnored) return true; // Ignore ignored files in parent state
const state = this.checkedItems.get(siblingPath);
return state === vscode.TreeItemCheckboxState.Checked;
})
).then((results) => results.every((res) => res));
let allChecked = true;

for (const sibling of siblings) {
const siblingPath = path.join(dirPath, sibling);
const isIgnored = this.isGitIgnored(
path.relative(this.workspaceRoot, siblingPath)
);
if (isIgnored) continue; // Ignore ignored files in parent state
const state =
this.checkedItems.get(siblingPath) ??
vscode.TreeItemCheckboxState.Unchecked;
if (state !== vscode.TreeItemCheckboxState.Checked) {
allChecked = false;
break;
}
}

if (allChecked) {
this.checkedItems.set(dirPath, vscode.TreeItemCheckboxState.Checked);
if (parentKey !== dirPath) {
await this.updateParentState(parentKey);
}
} else {
this.checkedItems.set(dirPath, vscode.TreeItemCheckboxState.Unchecked);
}
Expand Down Expand Up @@ -166,7 +173,31 @@ export class FileTreeProvider implements vscode.TreeDataProvider<FileItem> {
return Array.from(this.checkedItems.entries())
.filter(([_, state]) => state === vscode.TreeItemCheckboxState.Checked)
.map(([path, _]) => path)
.filter((path) => fs.statSync(path).isFile());
.filter((path) => fs.existsSync(path) && fs.statSync(path).isFile());
}

public async setCheckedFiles(filePaths: string[]): Promise<void> {
// Clear existing checks
this.checkedItems.clear();

// For each file in filePaths, set its checkboxState to Checked
for (const filePath of filePaths) {
if (fs.existsSync(filePath)) {
this.checkedItems.set(filePath, vscode.TreeItemCheckboxState.Checked);
}
}

// Update parent directories' checkbox states
for (const filePath of filePaths) {
let dirPath = path.dirname(filePath);
while (dirPath.startsWith(this.workspaceRoot)) {
await this.updateParentState(dirPath);
dirPath = path.dirname(dirPath);
}
}

// Refresh the tree view
this.refresh();
}

private loadGitignore() {
Expand Down

0 comments on commit 75edf43

Please sign in to comment.