Skip to content

Commit 75edf43

Browse files
ThomasThomas
authored andcommitted
Add file selection history feature
1 parent a589351 commit 75edf43

File tree

8 files changed

+199
-25
lines changed

8 files changed

+199
-25
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Copy file contents in XML format for LLM prompts effortlessly.
1212
- **Custom System Message**: Optionally include a system message in your copied output, encapsulated within a `<systemMessage>` XML element.
1313
- **Configurable Shortcuts**: Quickly refresh the file tree or copy files using customizable keyboard shortcuts.
1414
- **Git Ignore Support**: Automatically ignores files and directories specified in your .gitignore.
15+
- **Selection History**: Quickly switch between sets of previously selected files.
1516

1617
## Installation
1718

package.json

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "Files2Prompt",
44
"icon": "./files2prompt-icon.webp",
55
"description": "Copy file contents for LLM prompts",
6-
"version": "1.2.0",
6+
"version": "1.3.0",
77
"publisher": "thomas-mckanna",
88
"keywords": [
99
"files",
@@ -23,7 +23,7 @@
2323
"Other"
2424
],
2525
"activationEvents": [
26-
"onView.files2prompView"
26+
"onView.files2PromptView"
2727
],
2828
"main": "./out/extension.js",
2929
"contributes": {
@@ -68,6 +68,22 @@
6868
"light": "resources/light/clear.svg",
6969
"dark": "resources/dark/clear.svg"
7070
}
71+
},
72+
{
73+
"command": "files2prompt.goBack",
74+
"title": "Go Back",
75+
"icon": {
76+
"light": "resources/light/back.svg",
77+
"dark": "resources/dark/back.svg"
78+
}
79+
},
80+
{
81+
"command": "files2prompt.goForward",
82+
"title": "Go Forward",
83+
"icon": {
84+
"light": "resources/light/forward.svg",
85+
"dark": "resources/dark/forward.svg"
86+
}
7187
}
7288
],
7389
"menus": {
@@ -86,6 +102,16 @@
86102
"command": "files2prompt.clearChecks",
87103
"when": "view == files2PromptView",
88104
"group": "navigation@3"
105+
},
106+
{
107+
"command": "files2prompt.goBack",
108+
"when": "view == files2PromptView",
109+
"group": "navigation@4"
110+
},
111+
{
112+
"command": "files2prompt.goForward",
113+
"when": "view == files2PromptView",
114+
"group": "navigation@5"
89115
}
90116
]
91117
},

resources/dark/back.svg

Lines changed: 16 additions & 0 deletions
Loading

resources/dark/forward.svg

Lines changed: 16 additions & 0 deletions
Loading

resources/light/back.svg

Lines changed: 16 additions & 0 deletions
Loading

resources/light/forward.svg

Lines changed: 16 additions & 0 deletions
Loading

src/extension.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import * as vscode from "vscode";
44
import * as fs from "fs";
55
import * as path from "path";
6-
import { FileTreeProvider, FileItem } from "./fileTreeProvider";
6+
import { FileTreeProvider } from "./fileTreeProvider";
77

88
export function activate(context: vscode.ExtensionContext) {
99
const workspaceFolders = vscode.workspace.workspaceFolders;
@@ -17,6 +17,9 @@ export function activate(context: vscode.ExtensionContext) {
1717
manageCheckboxStateManually: true,
1818
});
1919

20+
let history: string[][] = [];
21+
let historyPosition: number = -1;
22+
2023
context.subscriptions.push(
2124
vscode.commands.registerCommand("files2prompt.refresh", () =>
2225
fileTreeProvider.refresh()
@@ -29,6 +32,17 @@ export function activate(context: vscode.ExtensionContext) {
2932
return;
3033
}
3134

35+
// Before saving the current selection to history, check if it's the same as the last selection
36+
const lastSelection = history[historyPosition] || [];
37+
if (!arraysEqual(checkedFiles, lastSelection)) {
38+
// Save the current selection to the history
39+
if (historyPosition < history.length - 1) {
40+
history = history.slice(0, historyPosition + 1);
41+
}
42+
history.push([...checkedFiles]); // Save a copy of the current selection
43+
historyPosition++;
44+
}
45+
3246
const xmlOutput = await generateXmlOutput(checkedFiles);
3347

3448
// Include system message if provided
@@ -53,8 +67,35 @@ export function activate(context: vscode.ExtensionContext) {
5367
vscode.commands.registerCommand("files2prompt.clearChecks", () => {
5468
fileTreeProvider.clearChecks();
5569
vscode.window.showInformationMessage("All checks have been cleared.");
70+
}),
71+
vscode.commands.registerCommand("files2prompt.goBack", async () => {
72+
if (historyPosition > 0) {
73+
historyPosition--;
74+
const previousSelection = history[historyPosition];
75+
76+
// Update the file selections in the FileTreeProvider
77+
await fileTreeProvider.setCheckedFiles(previousSelection);
78+
} else {
79+
// Show warning message
80+
vscode.window.showWarningMessage(
81+
"No previous selection to go back to."
82+
);
83+
}
84+
}),
85+
vscode.commands.registerCommand("files2prompt.goForward", async () => {
86+
if (historyPosition < history.length - 1) {
87+
historyPosition++;
88+
const nextSelection = history[historyPosition];
89+
90+
// Update the file selections in the FileTreeProvider
91+
await fileTreeProvider.setCheckedFiles(nextSelection);
92+
} else {
93+
// Show warning message
94+
vscode.window.showWarningMessage(
95+
"No next selection to go forward to."
96+
);
97+
}
5698
})
57-
// Add keybindings if necessary (optional)
5899
);
59100

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

98139
return `<files>\n${xmlContent}</files>`;
99140
}
141+
142+
// Helper function to compare arrays of strings
143+
function arraysEqual(a: string[], b: string[]): boolean {
144+
if (a.length !== b.length) return false;
145+
const sortedA = [...a].sort();
146+
const sortedB = [...b].sort();
147+
for (let i = 0; i < sortedA.length; i++) {
148+
if (sortedA[i] !== sortedB[i]) return false;
149+
}
150+
return true;
151+
}

src/fileTreeProvider.ts

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ export class FileTreeProvider implements vscode.TreeDataProvider<FileItem> {
3030
}
3131

3232
getTreeItem(element: FileItem): vscode.TreeItem {
33+
const key = element.resourceUri.fsPath;
34+
const checkboxState =
35+
this.checkedItems.get(key) ?? vscode.TreeItemCheckboxState.Unchecked;
36+
element.checkboxState = checkboxState;
3337
return element;
3438
}
3539

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

96100
if (item.isDirectory) {
97101
await this.updateDirectoryCheckState(key, state);
98-
} else {
99-
// If it's a file, update its parent directory's state
100-
const parentDir = path.dirname(key);
101-
await this.updateParentState(parentDir);
102+
}
103+
104+
// Update parent directories' states
105+
let dirPath = path.dirname(key);
106+
while (dirPath.startsWith(this.workspaceRoot)) {
107+
await this.updateParentState(dirPath);
108+
dirPath = path.dirname(dirPath);
102109
}
103110

104111
this.refresh();
105112
}
106113

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

112-
const allChecked = await Promise.all(
113-
siblings.map(async (sibling) => {
114-
const siblingPath = path.join(dirPath, sibling);
115-
const isIgnored = this.isGitIgnored(
116-
path.relative(this.workspaceRoot, siblingPath)
117-
);
118-
if (isIgnored) return true; // Ignore ignored files in parent state
119-
const state = this.checkedItems.get(siblingPath);
120-
return state === vscode.TreeItemCheckboxState.Checked;
121-
})
122-
).then((results) => results.every((res) => res));
117+
let allChecked = true;
118+
119+
for (const sibling of siblings) {
120+
const siblingPath = path.join(dirPath, sibling);
121+
const isIgnored = this.isGitIgnored(
122+
path.relative(this.workspaceRoot, siblingPath)
123+
);
124+
if (isIgnored) continue; // Ignore ignored files in parent state
125+
const state =
126+
this.checkedItems.get(siblingPath) ??
127+
vscode.TreeItemCheckboxState.Unchecked;
128+
if (state !== vscode.TreeItemCheckboxState.Checked) {
129+
allChecked = false;
130+
break;
131+
}
132+
}
123133

124134
if (allChecked) {
125135
this.checkedItems.set(dirPath, vscode.TreeItemCheckboxState.Checked);
126-
if (parentKey !== dirPath) {
127-
await this.updateParentState(parentKey);
128-
}
129136
} else {
130137
this.checkedItems.set(dirPath, vscode.TreeItemCheckboxState.Unchecked);
131138
}
@@ -166,7 +173,31 @@ export class FileTreeProvider implements vscode.TreeDataProvider<FileItem> {
166173
return Array.from(this.checkedItems.entries())
167174
.filter(([_, state]) => state === vscode.TreeItemCheckboxState.Checked)
168175
.map(([path, _]) => path)
169-
.filter((path) => fs.statSync(path).isFile());
176+
.filter((path) => fs.existsSync(path) && fs.statSync(path).isFile());
177+
}
178+
179+
public async setCheckedFiles(filePaths: string[]): Promise<void> {
180+
// Clear existing checks
181+
this.checkedItems.clear();
182+
183+
// For each file in filePaths, set its checkboxState to Checked
184+
for (const filePath of filePaths) {
185+
if (fs.existsSync(filePath)) {
186+
this.checkedItems.set(filePath, vscode.TreeItemCheckboxState.Checked);
187+
}
188+
}
189+
190+
// Update parent directories' checkbox states
191+
for (const filePath of filePaths) {
192+
let dirPath = path.dirname(filePath);
193+
while (dirPath.startsWith(this.workspaceRoot)) {
194+
await this.updateParentState(dirPath);
195+
dirPath = path.dirname(dirPath);
196+
}
197+
}
198+
199+
// Refresh the tree view
200+
this.refresh();
170201
}
171202

172203
private loadGitignore() {

0 commit comments

Comments
 (0)