Skip to content

Commit 777ae11

Browse files
Add XML paste feature
1 parent 75edf43 commit 777ae11

File tree

6 files changed

+160
-10
lines changed

6 files changed

+160
-10
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ node_modules
44
.vscode-test/
55
*.vsix
66
foobar
7-
.DS_Store
7+
.DS_Store
8+
docs/
9+
prompt

.vscodeignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ vsc-extension-quickstart.md
1010
**/*.ts
1111
**/.vscode-test.*
1212
!node_modules/ignore/**
13+
!node_modules/fast-xml-parser/**
1314
demo.gif

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Copy file contents in XML format for LLM prompts effortlessly.
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.
1515
- **Selection History**: Quickly switch between sets of previously selected files.
16+
- **Paste XML Response**: If XML is pasted into the editor in the same format as the copied output, the files will automatically be updated or created in your workspace. This allows for rapid iteration. See [recommended workflow](#recommended-workflow) for more information.
1617

1718
## Installation
1819

@@ -59,13 +60,29 @@ Copy file contents in XML format for LLM prompts effortlessly.
5960
- Windows/Linux: `Ctrl+Shift+C`
6061
- macOS: `Cmd+Shift+C`
6162

63+
### Paste XML Response:
64+
65+
- To update or create files in your workspace from LLM-outputted XML content, use the following keyboard shortcut while the extension UI is active:
66+
- Windows/Linux: `Ctrl+V`
67+
- macOS: `Cmd+V`
68+
69+
It is recommended that if you use this feature you have a custom system message (see section below) to ensure that the XML content is correctly formatted. Here is an example of such a system message:
70+
71+
```
72+
Always provide full code listings. You only need to include files that have changed. I repeat, ALWAYS provide full code listings. Your output should be in XML format (in a code block) that mirrors the input format (that is, `<files>` element with a list of `<file>` in the interior).
73+
```
74+
6275
### Include System Message:
6376

6477
1. Go to Settings (`Ctrl+,` or `Cmd+,` on macOS).
6578
2. Navigate to Extensions > Files2Prompt.
6679
3. Enter your custom system message in the System Message field.
6780
4. When you copy files, this message will be included at the top of the XML output.
6881

82+
See recommended system message above.
83+
84+
85+
6986
## Configuration
7087

7188
### Customizing Keyboard Shortcuts
@@ -106,3 +123,21 @@ This is a custom system message for LLM prompts.
106123
</file>
107124
</files>
108125
```
126+
127+
## Recommended Workflow
128+
129+
The Files2Prompt extension allows for a rapid iterative workflow when working with an LLM. It is first recommended that you set the system message setting to that described [above](#include-system-message). This will ensure that the XML content is correctly formatted when pasted back into the editor.
130+
131+
Next, create a file called `prompt` in the root of your repository. You will want to add this to `.gitignore`. This file acts as a scratchpad for you latest prompt.
132+
133+
Optionally, create a directory called `docs` (also in `.gitignore`) where you can paste documentation into files for any technologies or code that is related to your project.
134+
135+
Then, a single iteration of the workflow might look like this:
136+
137+
1. Alter the `prompt` file with you desired next state.
138+
2. Open the Files2Prompt view and select `prompt` and any other files that are relevant to your prompt. Also select any relevant documentation files from the `docs` directory.
139+
3. Copy your XML prompt with the keyboard shortcut or button.
140+
4. Paste the content into the LLM and submit.
141+
5. Copy the LLM contents, which should be in XML format if you followed the system message instructions.
142+
6. With the Files2Prompt view open, paste the LLM content with the keyboard shortcut. This will update the file contents or make brand new files.
143+
7. Loop back to step 1.

package-lock.json

Lines changed: 29 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 14 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.3.0",
6+
"version": "1.4.0",
77
"publisher": "thomas-mckanna",
88
"keywords": [
99
"files",
@@ -84,6 +84,10 @@
8484
"light": "resources/light/forward.svg",
8585
"dark": "resources/dark/forward.svg"
8686
}
87+
},
88+
{
89+
"command": "files2prompt.pasteXml",
90+
"title": "Paste XML"
8791
}
8892
],
8993
"menus": {
@@ -136,6 +140,13 @@
136140
"mac": "cmd+shift+c",
137141
"when": "files2PromptView.active && files2PromptView.visible && focusedView == 'files2PromptView'",
138142
"title": "Files2Prompt: Clear All Checks"
143+
},
144+
{
145+
"command": "files2prompt.pasteXml",
146+
"key": "ctrl+v",
147+
"mac": "cmd+v",
148+
"when": "files2PromptView.active && files2PromptView.visible && focusedView == 'files2PromptView'",
149+
"title": "Files2Prompt: Paste XML"
139150
}
140151
],
141152
"configuration": {
@@ -163,6 +174,7 @@
163174
"typescript": "^4.5.4"
164175
},
165176
"dependencies": {
177+
"fast-xml-parser": "^4.5.0",
166178
"ignore": "^6.0.2"
167179
}
168-
}
180+
}

src/extension.ts

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
// src/extension.ts
2-
31
import * as vscode from "vscode";
42
import * as fs from "fs";
53
import * as path from "path";
64
import { FileTreeProvider } from "./fileTreeProvider";
5+
import { XMLParser } from 'fast-xml-parser';
76

87
export function activate(context: vscode.ExtensionContext) {
98
const workspaceFolders = vscode.workspace.workspaceFolders;
@@ -52,8 +51,10 @@ export function activate(context: vscode.ExtensionContext) {
5251
let finalOutput = xmlOutput;
5352

5453
if (systemMessage && systemMessage.trim() !== "") {
54+
// Escape any ']]>' sequences in systemMessage
55+
const safeSystemMessage = systemMessage.replace(/]]>/g, ']]]]><![CDATA[>');
5556
finalOutput =
56-
`<systemMessage>\n<![CDATA[\n${systemMessage}\n]]>\n</systemMessage>\n` +
57+
`<systemMessage>\n<![CDATA[\n${safeSystemMessage}\n]]>\n</systemMessage>\n` +
5758
finalOutput;
5859
}
5960

@@ -95,6 +96,10 @@ export function activate(context: vscode.ExtensionContext) {
9596
"No next selection to go forward to."
9697
);
9798
}
99+
}),
100+
vscode.commands.registerCommand("files2prompt.pasteXml", async () => {
101+
const clipboardContent = await vscode.env.clipboard.readText();
102+
await processXmlContent(clipboardContent);
98103
})
99104
);
100105

@@ -120,20 +125,23 @@ export function activate(context: vscode.ExtensionContext) {
120125
}
121126
}
122127

123-
export function deactivate() {}
128+
export function deactivate() { }
124129

125130
// Helper function to generate XML output
126131
async function generateXmlOutput(filePaths: string[]): Promise<string> {
127132
let xmlContent = "";
128133

129134
for (const filePath of filePaths) {
130135
const content = fs.readFileSync(filePath, "utf-8");
136+
// Escape any ']]>' sequences in file content
137+
const safeContent = content.replace(/]]>/g, ']]]]><![CDATA[>');
138+
131139
const fileName = path.relative(
132140
vscode.workspace.workspaceFolders![0].uri.fsPath,
133141
filePath
134142
);
135143

136-
xmlContent += `<file name="${fileName}">\n<![CDATA[\n${content}\n]]>\n</file>\n`;
144+
xmlContent += `<file name="${fileName}">\n<![CDATA[\n${safeContent}\n]]>\n</file>\n`;
137145
}
138146

139147
return `<files>\n${xmlContent}</files>`;
@@ -149,3 +157,68 @@ function arraysEqual(a: string[], b: string[]): boolean {
149157
}
150158
return true;
151159
}
160+
161+
// Function to process XML content from clipboard
162+
async function processXmlContent(xmlContent: string) {
163+
// Extract only XML content
164+
const xmlOnly = extractXmlContent(xmlContent);
165+
if (!xmlOnly) {
166+
vscode.window.showErrorMessage("No valid XML content found in clipboard.");
167+
return;
168+
}
169+
170+
const parser = new XMLParser({
171+
ignoreAttributes: false,
172+
allowBooleanAttributes: true,
173+
parseTagValue: true,
174+
parseAttributeValue: false,
175+
trimValues: false,
176+
cdataPropName: '__cdata',
177+
tagValueProcessor: (val, tagName) => val, // Prevent default processing
178+
});
179+
180+
let jsonObj;
181+
try {
182+
jsonObj = parser.parse(xmlOnly);
183+
} catch (error) {
184+
vscode.window.showErrorMessage("Error parsing XML content from clipboard.");
185+
return;
186+
}
187+
188+
if (!jsonObj || !jsonObj.files || !jsonObj.files.file) {
189+
vscode.window.showErrorMessage("No <file> elements found in XML content.");
190+
return;
191+
}
192+
193+
const files = Array.isArray(jsonObj.files.file) ? jsonObj.files.file : [jsonObj.files.file];
194+
195+
for (const fileObj of files) {
196+
const fileName = fileObj['@_name'];
197+
let fileContent = '';
198+
199+
if (fileObj['__cdata']) {
200+
fileContent = fileObj['__cdata'];
201+
} else {
202+
// If no CDATA, get text content
203+
fileContent = fileObj['#text'] || '';
204+
}
205+
206+
if (fileName) {
207+
const filePath = path.join(vscode.workspace.workspaceFolders![0].uri.fsPath, fileName);
208+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
209+
await fs.promises.writeFile(filePath, fileContent, 'utf8');
210+
}
211+
}
212+
213+
vscode.window.showInformationMessage("Files have been updated successfully.");
214+
}
215+
216+
function extractXmlContent(text: string): string | null {
217+
const xmlMatch = text.match(/<files>[\s\S]*<\/files>/);
218+
if (xmlMatch) {
219+
return xmlMatch[0];
220+
}
221+
222+
// If no <files> tag found, return null
223+
return null;
224+
}

0 commit comments

Comments
 (0)