Compare commits
33 Commits
c5e16a6ae9
...
0.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
a31a1bd3e2
|
|||
|
d6450fd597
|
|||
|
c4f97e252d
|
|||
|
77e4df420e
|
|||
|
f6467a82c2
|
|||
|
d895f58963
|
|||
| d9e44b78ce | |||
|
4b38333d23
|
|||
|
bea2ea2a0f
|
|||
|
8246879bea
|
|||
| 72f21325dd | |||
|
7aece43a6b
|
|||
|
d2b09eefbe
|
|||
|
19a5dc658e
|
|||
|
f0f5b315e7
|
|||
|
5bb5236d3a
|
|||
|
7eb0d49d07
|
|||
|
69475782eb
|
|||
|
385e35a8ad
|
|||
|
2df0dc666b
|
|||
| c18e4dac10 | |||
| 5a6e1a5e90 | |||
| 134bdf1e5e | |||
| 5a4560758c | |||
| 926c88780b | |||
|
2d28f71e9b
|
|||
|
65bf168e1f
|
|||
|
6e3fbd8f2b
|
|||
|
3adfd712c0
|
|||
| 759cfad76e | |||
|
0ec4e8e2d2
|
|||
|
3676136692
|
|||
|
6bd50154f0
|
84
.gitea/workflows/release.yaml
Normal file
84
.gitea/workflows/release.yaml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
name: Release VSCode Extension
|
||||||
|
run-name: ${{ gitea.actor }} is releasing a new version 🚀
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Step 1: Checkout the repository code
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Step 2: Set up Node.js environment
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '23.9.0'
|
||||||
|
|
||||||
|
# Step 3: Install jq for JSON parsing
|
||||||
|
- name: Install jq
|
||||||
|
run: sudo apt-get install -y jq
|
||||||
|
|
||||||
|
# Step 4: Install project dependencies inside prompter directory
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
# Step 5: Install vsce
|
||||||
|
- name: Install vsce
|
||||||
|
run: npm install @vscode/vsce
|
||||||
|
|
||||||
|
# Step 6: Install additional dev dependencies for building inside prompter directory
|
||||||
|
- name: Install build tools
|
||||||
|
run: npm install --save-dev webpack webpack-cli ts-loader codegen
|
||||||
|
|
||||||
|
# Step 7: Build the extension inside prompter directory
|
||||||
|
- name: Build the extension
|
||||||
|
run: npm run webpack:prod
|
||||||
|
|
||||||
|
# Step 8: Package the extension into a VSIX file inside prompter directory
|
||||||
|
- name: Package the extension
|
||||||
|
run: npx vsce package
|
||||||
|
|
||||||
|
# Step 9: Extract the tag name from gitea.ref
|
||||||
|
- name: Extract tag name
|
||||||
|
run: |
|
||||||
|
TAG_NAME=$(echo "${{ gitea.ref }}" | sed 's/refs\/tags\///')
|
||||||
|
echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Step 10: Determine the VSIX file name from package.json inside prompter directory
|
||||||
|
- name: Get VSIX file name
|
||||||
|
run: |
|
||||||
|
NAME=$(jq -r .name package.json)
|
||||||
|
VERSION=$(jq -r .version package.json)
|
||||||
|
VSIX_FILE="${NAME}-${VERSION}.vsix"
|
||||||
|
echo "VSIX_FILE=$VSIX_FILE" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Step 11: Create a new release in Gitea
|
||||||
|
- name: Create release
|
||||||
|
run: |
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: token ${{ gitea.token }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\": \"$TAG_NAME\", \"name\": \"Release $TAG_NAME\", \"body\": \"Automated release for version $TAG_NAME\"}" \
|
||||||
|
https://git.bhakat.dev/api/v1/repos/abhishekbhakat/Prompter/releases
|
||||||
|
|
||||||
|
# Step 12: Retrieve the release ID
|
||||||
|
- name: Get release ID
|
||||||
|
run: |
|
||||||
|
RELEASE_ID=$(curl -s -H "Authorization: token ${{ gitea.token }}" \
|
||||||
|
https://git.bhakat.dev/api/v1/repos/abhishekbhakat/Prompter/releases/tags/$TAG_NAME | jq .id)
|
||||||
|
echo "RELEASE_ID=$RELEASE_ID" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Step 13: Upload the VSIX file as a release asset (from prompter directory)
|
||||||
|
- name: Upload VSIX to release
|
||||||
|
run: |
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: token ${{ gitea.token }}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @"$VSIX_FILE" \
|
||||||
|
https://git.bhakat.dev/api/v1/repos/abhishekbhakat/Prompter/releases/$RELEASE_ID/assets?name=$VSIX_FILE
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -137,3 +137,5 @@ dist
|
|||||||
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
*.vsix
|
*.vsix
|
||||||
|
|
||||||
|
attached_assets
|
||||||
|
|||||||
12
.vscode-test.mjs
Normal file
12
.vscode-test.mjs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
export default {
|
||||||
|
version: 'stable',
|
||||||
|
extensionDevelopmentPath: __dirname,
|
||||||
|
extensionTestsPath: path.join(__dirname, 'out', 'test'),
|
||||||
|
testFiles: ['**/**.test.js'],
|
||||||
|
workspaceFolder: __dirname
|
||||||
|
};
|
||||||
@@ -8,6 +8,8 @@ src/**
|
|||||||
**/.eslintrc.json
|
**/.eslintrc.json
|
||||||
**/*.map
|
**/*.map
|
||||||
**/*.ts
|
**/*.ts
|
||||||
|
out/**
|
||||||
|
webpack.config.js
|
||||||
.replit
|
.replit
|
||||||
.breakpoints
|
.breakpoints
|
||||||
.local/**
|
.local/**
|
||||||
@@ -25,4 +27,5 @@ generated-icon.png
|
|||||||
out/test/**
|
out/test/**
|
||||||
package-lock.json
|
package-lock.json
|
||||||
# Assets not needed in the final package
|
# Assets not needed in the final package
|
||||||
attached_assets/**
|
attached_assets/**
|
||||||
|
.config
|
||||||
|
|||||||
@@ -6,4 +6,10 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
- Initial release
|
- Initial release
|
||||||
|
|
||||||
|
## [0.0.1] - 2025-03-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- PromptGeneration extension with basic file selection and xml edits option.
|
||||||
|
|||||||
161
README.md
161
README.md
@@ -1,153 +1,48 @@
|
|||||||
# Prompter
|
# Prompter
|
||||||
|
|
||||||
Code smarter with AI—no more messy copy-pasting. Prompter structures your prompts and applies AI changes seamlessly, streamlining your coding workflow.
|
Enhance your coding with AI—Prompter organizes your prompts and implements AI changes effortlessly, improving your development workflow.
|
||||||
|
|
||||||
## Why Prompter?
|
## Why Use Prompter?
|
||||||
|
|
||||||
- **Too much bloat in your repo?** Stop zipping everything—send only the files that matter.
|
- **Reduce repository clutter** - Share only essential files instead of everything
|
||||||
- **LLM underperforming?** Cut the noise for sharper, more accurate responses.
|
- **Improve AI responses** - Eliminate irrelevant context for better results
|
||||||
- **Better AI coding?** Select just the right context to optimize results.
|
- **Optimize coding assistance** - Target exactly what you need for precise AI help
|
||||||
|
|
||||||
Prompter empowers you to work efficiently with AI, reducing token waste and improving clarity.
|
Prompter helps you collaborate efficiently with AI, minimizing token usage and enhancing clarity.
|
||||||
|
|
||||||
## Features
|
## Key Features
|
||||||
|
|
||||||
- **Advanced File Selection & Token Estimation**
|
- **Smart File Selection & Token Counting**
|
||||||
Precisely filter files and estimate token usage instantly for optimized, cost-effective prompts.
|
Filter files and track token usage for efficient, economical prompts
|
||||||
|
|
||||||
- **Optimized XML Prompt**
|
- **XML-Based Prompt Structure**
|
||||||
Structured file trees, CodeMaps, content, and instructions in XML for maximum LLM clarity.
|
Organize file trees, CodeMaps, and instructions in XML for better AI comprehension
|
||||||
|
|
||||||
- **Structured XML Diffs**
|
- **Clean XML Diff Application**
|
||||||
Converts LLM-generated XML edits into precise, reviewable diffs—works at any file size.
|
Transform AI-generated XML changes into clear, reviewable diffs at any scale
|
||||||
|
|
||||||
- **Codemap Extraction**
|
- **Intelligent Code Scanning**
|
||||||
Scans files locally to extract classes, functions, and references, minimizing tokens and hallucinations. Auto-detects referenced types.
|
Extract code structure locally to reduce tokens and prevent hallucinations
|
||||||
|
|
||||||
- **Mac-Native Performance**
|
- **Cross-Platform Support**
|
||||||
Built for macOS with native speed and responsiveness—because performance matters.
|
Designed for any VSCode platform with native performance and responsiveness
|
||||||
|
|
||||||
- **Clipboard Integration**
|
- **Direct Clipboard Support**
|
||||||
Copy structured prompts into any AI chat app—your data stays local, no external API needed.
|
Copy structured prompts to any AI platform with your data remaining local
|
||||||
|
|
||||||
- **Works with Any Model**
|
- **Privacy-Focused Design**
|
||||||
Compatible with OpenAI, Anthropic, DeepSeek, Gemini, Azure, OpenRouter, and local models—private and offline when you need it.
|
Process everything locally without sending data to third parties
|
||||||
|
|
||||||
- **Privacy First**
|
## Quick Start
|
||||||
Local models, offline scanning, and direct clipboard use—no intermediaries required.
|
|
||||||
|
|
||||||
## Installation
|
1. Install the extension in VS Code
|
||||||
|
2. Select relevant files through the Prompter sidebar
|
||||||
*(Note: Installation steps are assumed based on the VS Code context from other files. Adjust as needed.)*
|
3. Generate and copy a structured XML prompt
|
||||||
1. Clone the repository:
|
4. Paste into your AI tool of choice
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
```
|
|
||||||
2. Open the project in VS Code.
|
|
||||||
3. Install dependencies:
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
4. Build the extension:
|
|
||||||
```bash
|
|
||||||
npm run compile
|
|
||||||
```
|
|
||||||
5. Press `F5` in VS Code to launch the extension in a development window.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
1. Open your project in VS Code.
|
|
||||||
2. Use the Prompter interface to select files and estimate tokens.
|
|
||||||
3. Generate a structured XML prompt via the clipboard.
|
|
||||||
4. Paste into your preferred AI model (e.g., ChatGPT, Claude, or a local LLM).
|
|
||||||
5. Apply the returned XML diffs directly through Prompter for seamless integration.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
We welcome contributions! To get started:
|
Contributions welcome!
|
||||||
1. Fork the repository.
|
|
||||||
2. Create a feature branch: `git checkout -b my-feature`.
|
|
||||||
3. Commit your changes: `git commit -m "Add my feature"`.
|
|
||||||
4. Push to your branch: `git push origin my-feature`.
|
|
||||||
5. Open a pull request.
|
|
||||||
|
|
||||||
See `vsc-extension-quickstart.md` for development setup and testing details.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
Built with ❤️ by the Prompter team.
|
Built with ❤️ by the Prompter team.
|
||||||
|
|
||||||
Code smarter with AI—no more messy copy-pasting. Prompter structures your prompts and applies AI changes seamlessly, streamlining your coding workflow.
|
|
||||||
|
|
||||||
## Why Prompter?
|
|
||||||
|
|
||||||
- **Too much bloat in your repo?** Stop zipping everything—send only the files that matter.
|
|
||||||
- **LLM underperforming?** Cut the noise for sharper, more accurate responses.
|
|
||||||
- **Better AI coding?** Select just the right context to optimize results.
|
|
||||||
|
|
||||||
Prompter empowers you to work efficiently with AI, reducing token waste and improving clarity.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Advanced File Selection & Token Estimation**
|
|
||||||
Precisely filter files and estimate token usage instantly for optimized, cost-effective prompts.
|
|
||||||
|
|
||||||
- **Optimized XML Prompt**
|
|
||||||
Structured file trees, CodeMaps, content, and instructions in XML for maximum LLM clarity.
|
|
||||||
|
|
||||||
- **Structured XML Diffs**
|
|
||||||
Converts LLM-generated XML edits into precise, reviewable diffs—works at any file size.
|
|
||||||
|
|
||||||
- **Codemap Extraction**
|
|
||||||
Scans files locally to extract classes, functions, and references, minimizing tokens and hallucinations. Auto-detects referenced types.
|
|
||||||
|
|
||||||
- **Mac-Native Performance**
|
|
||||||
Built for macOS with native speed and responsiveness—because performance matters.
|
|
||||||
|
|
||||||
- **Clipboard Integration**
|
|
||||||
Copy structured prompts into any AI chat app—your data stays local, no external API needed.
|
|
||||||
|
|
||||||
- **Works with Any Model**
|
|
||||||
Compatible with OpenAI, Anthropic, DeepSeek, Gemini, Azure, OpenRouter, and local models—private and offline when you need it.
|
|
||||||
|
|
||||||
- **Privacy First**
|
|
||||||
Local models, offline scanning, and direct clipboard use—no intermediaries required.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
*(Note: Installation steps are assumed based on the VS Code context from other files. Adjust as needed.)*
|
|
||||||
1. Clone the repository:
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
```
|
|
||||||
2. Open the project in VS Code.
|
|
||||||
3. Install dependencies:
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
4. Build the extension:
|
|
||||||
```bash
|
|
||||||
npm run compile
|
|
||||||
```
|
|
||||||
5. Press `F5` in VS Code to launch the extension in a development window.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
1. Open your project in VS Code.
|
|
||||||
2. Use the Prompter interface to select files and estimate tokens.
|
|
||||||
3. Generate a structured XML prompt via the clipboard.
|
|
||||||
4. Paste into your preferred AI model (e.g., ChatGPT, Claude, or a local LLM).
|
|
||||||
5. Apply the returned XML diffs directly through Prompter for seamless integration.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
We welcome contributions! To get started:
|
|
||||||
1. Fork the repository.
|
|
||||||
2. Create a feature branch: `git checkout -b my-feature`.
|
|
||||||
3. Commit your changes: `git commit -m "Add my feature"`.
|
|
||||||
4. Push to your branch: `git push origin my-feature`.
|
|
||||||
5. Open a pull request.
|
|
||||||
|
|
||||||
See `vsc-extension-quickstart.md` for development setup and testing details.
|
|
||||||
|
|
||||||
---
|
|
||||||
Built with ❤️ by the Prompter team.
|
|
||||||
19
jest.config.js
Normal file
19
jest.config.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} **/
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
transform: {
|
||||||
|
'^.+\.tsx?$': ['ts-jest', {}],
|
||||||
|
},
|
||||||
|
testMatch: ['**/test/**/*.ts'],
|
||||||
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||||
|
globals: {
|
||||||
|
'ts-jest': {
|
||||||
|
tsconfig: 'tsconfig.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Mock the vscode module since it's not available during testing
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^vscode$': '<rootDir>/src/test/vscode-mock.ts'
|
||||||
|
},
|
||||||
|
};
|
||||||
50
package.json
50
package.json
@@ -2,8 +2,9 @@
|
|||||||
"name": "prompter",
|
"name": "prompter",
|
||||||
"displayName": "Prompter",
|
"displayName": "Prompter",
|
||||||
"description": "Easy prompt generation and apply edits using prompter.",
|
"description": "Easy prompt generation and apply edits using prompter.",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"publisher": "abhishekbhakat",
|
"publisher": "abhishekbhakat",
|
||||||
|
"repository": "https://git.bhakat.dev/abhishekbhakat/Prompter",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.98.0"
|
"vscode": "^1.98.0"
|
||||||
},
|
},
|
||||||
@@ -11,7 +12,7 @@
|
|||||||
"Other"
|
"Other"
|
||||||
],
|
],
|
||||||
"activationEvents": [],
|
"activationEvents": [],
|
||||||
"main": "./out/extension.js",
|
"main": "./dist/extension.js",
|
||||||
"contributes": {
|
"contributes": {
|
||||||
"commands": [
|
"commands": [
|
||||||
{
|
{
|
||||||
@@ -20,7 +21,18 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "prompter.generatePrompt",
|
"command": "prompter.generatePrompt",
|
||||||
"title": "Copy"
|
"title": "Copy",
|
||||||
|
"icon": "$(clippy)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "prompter.openTreeView",
|
||||||
|
"title": "Show Tree View",
|
||||||
|
"icon": "$(list-tree)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "prompter.openSettings",
|
||||||
|
"title": "Settings",
|
||||||
|
"icon": "$(settings-gear)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"viewsContainers": {
|
"viewsContainers": {
|
||||||
@@ -36,7 +48,8 @@
|
|||||||
"prompter-sidebar": [
|
"prompter-sidebar": [
|
||||||
{
|
{
|
||||||
"id": "prompterView",
|
"id": "prompterView",
|
||||||
"name": "Prompter"
|
"name": "Prompter",
|
||||||
|
"icon": "resources/icon.svg"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -44,7 +57,18 @@
|
|||||||
"view/title": [
|
"view/title": [
|
||||||
{
|
{
|
||||||
"command": "prompter.generatePrompt",
|
"command": "prompter.generatePrompt",
|
||||||
"group": "navigation"
|
"group": "navigation@1",
|
||||||
|
"when": "view == prompterView"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "prompter.openTreeView",
|
||||||
|
"group": "navigation@2",
|
||||||
|
"when": "view == prompterView"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "prompter.openSettings",
|
||||||
|
"group": "navigation@3",
|
||||||
|
"when": "view == prompterView"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"view/item/context": [
|
"view/item/context": [
|
||||||
@@ -56,12 +80,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"vscode:prepublish": "npm run compile",
|
"vscode:prepublish": "npm run webpack:prod",
|
||||||
|
"webpack:dev": "webpack --mode development",
|
||||||
|
"webpack:prod": "webpack --mode production",
|
||||||
"compile": "tsc -p ./",
|
"compile": "tsc -p ./",
|
||||||
"watch": "tsc -watch -p ./",
|
"watch": "webpack --watch --mode development",
|
||||||
"pretest": "npm run compile && npm run lint",
|
"pretest": "npm run compile && npm run lint",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"test": "vscode-test"
|
"test": "node ./src/test/runTest.mjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mocha": "^10.0.10",
|
"@types/mocha": "^10.0.10",
|
||||||
@@ -71,10 +97,14 @@
|
|||||||
"@typescript-eslint/parser": "^8.25.0",
|
"@typescript-eslint/parser": "^8.25.0",
|
||||||
"@vscode/test-cli": "^0.0.10",
|
"@vscode/test-cli": "^0.0.10",
|
||||||
"@vscode/test-electron": "^2.4.1",
|
"@vscode/test-electron": "^2.4.1",
|
||||||
|
"ajv": "^8.17.0",
|
||||||
"eslint": "^9.21.0",
|
"eslint": "^9.21.0",
|
||||||
"typescript": "^5.7.3"
|
"ts-loader": "^9.5.2",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"webpack": "^5.98.0",
|
||||||
|
"webpack-cli": "^6.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vscode/vsce": "^3.2.2"
|
"ignore": "^7.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
294
resources/xml_formatting_instructions.xml
Normal file
294
resources/xml_formatting_instructions.xml
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
<xml_formatting_instructions>
|
||||||
|
### Role
|
||||||
|
- You are a **code editing assistant**: You can fulfill edit requests and chat with the user about code or other questions. Provide complete instructions or code lines when replying with xml formatting.
|
||||||
|
|
||||||
|
### Capabilities
|
||||||
|
- Can create new files.
|
||||||
|
- Can rewrite entire files.
|
||||||
|
- Can perform partial search/replace modifications.
|
||||||
|
- Can delete existing files.
|
||||||
|
|
||||||
|
Avoid placeholders like `...` or `// existing code here`. Provide complete lines or code.
|
||||||
|
|
||||||
|
## Tools & Actions
|
||||||
|
1. **create** – Create a new file if it doesn’t exist.
|
||||||
|
2. **rewrite** – Replace the entire content of an existing file.
|
||||||
|
3. **modify** (search/replace) – For partial edits with <search> + <content>.
|
||||||
|
4. **delete** – Remove a file entirely (empty <content>).
|
||||||
|
|
||||||
|
### **Format to Follow for Diff Protocol**
|
||||||
|
|
||||||
|
<Plan>
|
||||||
|
Describe your approach or reasoning here.
|
||||||
|
</Plan>
|
||||||
|
|
||||||
|
<file path="path/to/example.swift" action="one_of_the_tools">
|
||||||
|
<change>
|
||||||
|
<description>Brief explanation of this specific change</description>
|
||||||
|
<search>
|
||||||
|
===
|
||||||
|
// Exactly matching lines to find
|
||||||
|
===
|
||||||
|
</search>
|
||||||
|
<content>
|
||||||
|
===
|
||||||
|
// Provide the new or updated code here. Do not use placeholders
|
||||||
|
===
|
||||||
|
</content>
|
||||||
|
</change>
|
||||||
|
<!-- Add more <change> blocks if you have multiple edits for the same file -->
|
||||||
|
</file>
|
||||||
|
|
||||||
|
#### Tools Demonstration
|
||||||
|
1. `<file path="NewFile.swift" action="create">` – Full file in <content>
|
||||||
|
2. `<file path="DeleteMe.swift" action="delete">` – Empty <content>
|
||||||
|
3. `<file path="ModifyMe.swift" action="modify">` – Partial edit with `<search>` + `<content>`
|
||||||
|
4. `<file path="RewriteMe.swift" action="rewrite">` – Entire file in <content>
|
||||||
|
5. `<file path="RewriteMe.swift" action="rewrite">` – Entire file in <content>. No <search> required.
|
||||||
|
|
||||||
|
## Format Guidelines
|
||||||
|
1. **Plan**: Begin with a `<Plan>` block explaining your approach.
|
||||||
|
2. **<file> Tag**: e.g. `<file path="Models/User.swift" action="...">`. Must match an available tool.
|
||||||
|
3. **<change> Tag**: Provide `<description>` to clarify each change. Then `<content>` for new/modified code. Additional rules depend on your capabilities.
|
||||||
|
4. **modify**: **<search> & <content>**: Provide code blocks enclosed by ===. Respect indentation exactly, ensuring the <search> block matches the original source down to braces, spacing, and any comments. The new <content> will replace the <search> block, and should should fit perfectly in the space left by it's removal.
|
||||||
|
5. **modify**: For changes to the same file, ensure that you use multiple change blocks, rather than separate file blocks.
|
||||||
|
6. **rewrite**: For large overhauls; omit `<search>` and put the entire file in `<content>`.
|
||||||
|
7. **create**: For new files, put the full file in <content>.
|
||||||
|
8. **delete**: Provide an empty <content>. The file is removed.
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
|
||||||
|
-----
|
||||||
|
### Example: Search and Replace (Add email property)
|
||||||
|
<Plan>
|
||||||
|
Add an email property to `User` via search/replace.
|
||||||
|
</Plan>
|
||||||
|
|
||||||
|
<file path="Models/User.swift" action="modify">
|
||||||
|
<change>
|
||||||
|
<description>Add email property to User struct</description>
|
||||||
|
<search>
|
||||||
|
===
|
||||||
|
struct User {
|
||||||
|
let id: UUID
|
||||||
|
var name: String
|
||||||
|
}
|
||||||
|
===
|
||||||
|
</search>
|
||||||
|
<content>
|
||||||
|
===
|
||||||
|
struct User {
|
||||||
|
let id: UUID
|
||||||
|
var name: String
|
||||||
|
var email: String
|
||||||
|
}
|
||||||
|
===
|
||||||
|
</content>
|
||||||
|
</change>
|
||||||
|
</file>
|
||||||
|
|
||||||
|
-----
|
||||||
|
### Example: Negative Example - Mismatched Search Block
|
||||||
|
// Example Input (not part of final output, just demonstration)
|
||||||
|
<file_contents>
|
||||||
|
File: path/service.swift
|
||||||
|
```
|
||||||
|
import Foundation
|
||||||
|
class Example {
|
||||||
|
foo() {
|
||||||
|
Bar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</file_contents>
|
||||||
|
|
||||||
|
<Plan>
|
||||||
|
Demonstrate how a mismatched search block leads to failed merges.
|
||||||
|
</Plan>
|
||||||
|
|
||||||
|
<file path="path/service.swift" action="modify">
|
||||||
|
<change>
|
||||||
|
<description>This search block is missing or has mismatched indentation, braces, etc.</description>
|
||||||
|
<search>
|
||||||
|
===
|
||||||
|
foo() {
|
||||||
|
Bar()
|
||||||
|
}
|
||||||
|
===
|
||||||
|
</search>
|
||||||
|
<content>
|
||||||
|
===
|
||||||
|
foo() {
|
||||||
|
Bar()
|
||||||
|
Bar2()
|
||||||
|
}
|
||||||
|
===
|
||||||
|
</content>
|
||||||
|
</change>
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<!-- This example fails because the <search> block doesn't exactly match the original file contents. -->
|
||||||
|
|
||||||
|
-----
|
||||||
|
### Example: Negative Example - Mismatched Brace Balance
|
||||||
|
// This negative example shows how adding extra braces in the <content> can break brace matching.
|
||||||
|
<Plan>
|
||||||
|
Demonstrate that the new content block has one extra closing brace, causing mismatched braces.
|
||||||
|
</Plan>
|
||||||
|
|
||||||
|
<file path="Functions/MismatchedBracesExample.swift" action="modify">
|
||||||
|
<change>
|
||||||
|
<description>Mismatched brace balance in the replacement content</description>
|
||||||
|
<search>
|
||||||
|
===
|
||||||
|
foo() {
|
||||||
|
Bar()
|
||||||
|
}
|
||||||
|
===
|
||||||
|
</search>
|
||||||
|
<content>
|
||||||
|
===
|
||||||
|
foo() {
|
||||||
|
Bar()
|
||||||
|
}
|
||||||
|
|
||||||
|
bar() {
|
||||||
|
foo2()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
===
|
||||||
|
</content>
|
||||||
|
</change>
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<!-- Because the <search> block was only a small brace segment, adding extra braces in <content> breaks the balance. -->
|
||||||
|
|
||||||
|
-----
|
||||||
|
### Example: Negative Example - One-Line Search Block
|
||||||
|
<Plan>
|
||||||
|
Demonstrate a one-line search block, which is too short to be reliable.
|
||||||
|
</Plan>
|
||||||
|
|
||||||
|
<file path="path/service.swift" action="modify">
|
||||||
|
<change>
|
||||||
|
<description>One-line search block is ambiguous</description>
|
||||||
|
<search>
|
||||||
|
===
|
||||||
|
var email: String
|
||||||
|
===
|
||||||
|
</search>
|
||||||
|
<content>
|
||||||
|
===
|
||||||
|
var emailNew: String
|
||||||
|
===
|
||||||
|
</content>
|
||||||
|
</change>
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<!-- This example fails because the <search> block is only one line and ambiguous. -->
|
||||||
|
|
||||||
|
-----
|
||||||
|
### Example: Negative Example - Ambiguous Search Block
|
||||||
|
<Plan>
|
||||||
|
Demonstrate an ambiguous search block that can match multiple blocks (e.g., multiple closing braces).
|
||||||
|
</Plan>
|
||||||
|
|
||||||
|
<file path="path/service.swift" action="modify">
|
||||||
|
<change>
|
||||||
|
<description>Ambiguous search block with multiple closing braces</description>
|
||||||
|
<search>
|
||||||
|
===
|
||||||
|
}
|
||||||
|
}
|
||||||
|
===
|
||||||
|
</search>
|
||||||
|
<content>
|
||||||
|
===
|
||||||
|
foo() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
===
|
||||||
|
</content>
|
||||||
|
</change>
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<!-- This example fails because the <search> block is ambiguous due to multiple matching closing braces. -->
|
||||||
|
|
||||||
|
-----
|
||||||
|
### Example: Full File Rewrite
|
||||||
|
<Plan>
|
||||||
|
Rewrite the entire User file to include an email property.
|
||||||
|
</Plan>
|
||||||
|
|
||||||
|
<file path="Models/User.swift" action="rewrite">
|
||||||
|
<change>
|
||||||
|
<description>Full file rewrite with new email field</description>
|
||||||
|
<content>
|
||||||
|
===
|
||||||
|
import Foundation
|
||||||
|
struct User {
|
||||||
|
let id: UUID
|
||||||
|
var name: String
|
||||||
|
var email: String
|
||||||
|
|
||||||
|
init(name: String, email: String) {
|
||||||
|
self.id = UUID()
|
||||||
|
self.name = name
|
||||||
|
self.email = email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
===
|
||||||
|
</content>
|
||||||
|
</change>
|
||||||
|
</file>
|
||||||
|
|
||||||
|
-----
|
||||||
|
### Example: Create New File
|
||||||
|
<Plan>
|
||||||
|
Create a new RoundedButton for a custom Swift UIButton subclass.
|
||||||
|
</Plan>
|
||||||
|
|
||||||
|
<file path="Views/RoundedButton.swift" action="create">
|
||||||
|
<change>
|
||||||
|
<description>Create custom RoundedButton class</description>
|
||||||
|
<content>
|
||||||
|
===
|
||||||
|
import UIKit
|
||||||
|
@IBDesignable
|
||||||
|
class RoundedButton: UIButton {
|
||||||
|
@IBInspectable var cornerRadius: CGFloat = 0
|
||||||
|
}
|
||||||
|
===
|
||||||
|
</content>
|
||||||
|
</change>
|
||||||
|
</file>
|
||||||
|
|
||||||
|
-----
|
||||||
|
### Example: Delete a File
|
||||||
|
<Plan>
|
||||||
|
Remove an obsolete file.
|
||||||
|
</Plan>
|
||||||
|
|
||||||
|
<file path="Obsolete/File.swift" action="delete">
|
||||||
|
<change>
|
||||||
|
<description>Completely remove the file from the project</description>
|
||||||
|
<content>
|
||||||
|
===
|
||||||
|
===
|
||||||
|
</content>
|
||||||
|
</change>
|
||||||
|
</file>
|
||||||
|
|
||||||
|
## Final Notes
|
||||||
|
1. **modify** Always wrap the exact original lines in <search> and your updated lines in <content>, each enclosed by ===.
|
||||||
|
2. **modify** The <search> block must match the source code exactly—down to indentation, braces, spacing, and any comments. Even a minor mismatch causes failed merges.
|
||||||
|
3. **modify** Only replace exactly what you need. Avoid including entire functions or files if only a small snippet changes, and ensure the <search> content is unique and easy to identify.
|
||||||
|
4. **rewrite** Use `rewrite` for major overhauls, and `modify` for smaller, localized edits. Rewrite requires the entire code to be replaced, so use it sparingly.
|
||||||
|
5. You can always **create** new files and **delete** existing files. Provide full code for create, and empty content for delete. Avoid creating files you know exist already.
|
||||||
|
6. If a file tree is provided, place your files logically within that structure. Respect the user’s relative or absolute paths.
|
||||||
|
7. Wrap your final output in ```XML ... ``` for clarity.
|
||||||
|
8. **Important:** Do not wrap any XML output in CDATA tags (i.e. `<![CDATA[ ... ]]>`). We xpect raw XML exactly as shown in the examples.
|
||||||
|
9. **IMPORTANT** IF MAKING FILE CHANGES, YOU MUST USE THE AVAILABLE XML FORMATTING CAPABILITIES PROVIDED ABOVE - IT IS THE ONLY WAY FOR YOUR CHANGES TO BE APPLIED.
|
||||||
|
10. The final output must apply cleanly with no leftover syntax errors.
|
||||||
|
</xml_formatting_instructions>
|
||||||
319
src/extension.ts
319
src/extension.ts
@@ -1,210 +1,98 @@
|
|||||||
// The module 'vscode' contains the VS Code extensibility API
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
// Custom TreeItem for files and folders
|
// Import our modular components
|
||||||
class FileTreeItem extends vscode.TreeItem {
|
import { PrompterSettings, DEFAULT_SETTINGS } from './models/settings';
|
||||||
constructor(
|
import { FileTreeItem, SettingTreeItem } from './models/treeItems';
|
||||||
public readonly resourceUri: vscode.Uri,
|
import { PrompterTreeProvider } from './providers/prompterTreeProvider';
|
||||||
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
|
import { PromptGenerator } from './utils/promptGenerator';
|
||||||
public readonly isDirectory: boolean
|
|
||||||
) {
|
|
||||||
super(resourceUri, collapsibleState);
|
|
||||||
this.contextValue = isDirectory ? 'directory' : 'file';
|
|
||||||
this.tooltip = resourceUri.fsPath;
|
|
||||||
this.description = isDirectory ? 'folder' : path.extname(resourceUri.fsPath);
|
|
||||||
this.checkboxState = vscode.TreeItemCheckboxState.Unchecked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TreeView provider for the sidebar
|
/**
|
||||||
class PrompterTreeProvider implements vscode.TreeDataProvider<FileTreeItem> {
|
* This method is called when the extension is activated
|
||||||
private _onDidChangeTreeData: vscode.EventEmitter<FileTreeItem | undefined | null | void> = new vscode.EventEmitter<FileTreeItem | undefined | null | void>();
|
*/
|
||||||
readonly onDidChangeTreeData: vscode.Event<FileTreeItem | undefined | null | void> = this._onDidChangeTreeData.event;
|
|
||||||
private selectedFiles: Set<string> = new Set();
|
|
||||||
private xmlEditsEnabled: boolean = false;
|
|
||||||
|
|
||||||
constructor(private workspaceRoot: string | undefined) {}
|
|
||||||
|
|
||||||
getTreeItem(element: FileTreeItem): vscode.TreeItem {
|
|
||||||
const item = element;
|
|
||||||
if (this.selectedFiles.has(element.resourceUri.fsPath)) {
|
|
||||||
item.checkboxState = vscode.TreeItemCheckboxState.Checked;
|
|
||||||
item.label = path.basename(element.resourceUri.fsPath);
|
|
||||||
} else {
|
|
||||||
item.checkboxState = vscode.TreeItemCheckboxState.Unchecked;
|
|
||||||
item.label = path.basename(element.resourceUri.fsPath);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getChildren(element?: FileTreeItem): Promise<FileTreeItem[]> {
|
|
||||||
if (!this.workspaceRoot) {
|
|
||||||
vscode.window.showInformationMessage('No workspace folder is opened');
|
|
||||||
return Promise.resolve([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
const dirPath = element.resourceUri.fsPath;
|
|
||||||
return this.getFilesInDirectory(dirPath);
|
|
||||||
} else {
|
|
||||||
return this.getFilesInDirectory(this.workspaceRoot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getFilesInDirectory(dirPath: string): Promise<FileTreeItem[]> {
|
|
||||||
try {
|
|
||||||
const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath));
|
|
||||||
return Promise.all(files.map(async ([name, type]) => {
|
|
||||||
const filePath = path.join(dirPath, name);
|
|
||||||
const uri = vscode.Uri.file(filePath);
|
|
||||||
const isDirectory = (type & vscode.FileType.Directory) !== 0;
|
|
||||||
|
|
||||||
return new FileTreeItem(
|
|
||||||
uri,
|
|
||||||
isDirectory ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None,
|
|
||||||
isDirectory
|
|
||||||
);
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error reading directory ${dirPath}:`, error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleSelection(item: FileTreeItem): void {
|
|
||||||
const filePath = item.resourceUri.fsPath;
|
|
||||||
console.log('Toggle selection called for:', filePath);
|
|
||||||
if (this.selectedFiles.has(filePath)) {
|
|
||||||
this.removeFromSelection(item);
|
|
||||||
} else {
|
|
||||||
this.addToSelection(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addToSelection(item: FileTreeItem): void {
|
|
||||||
const filePath = item.resourceUri.fsPath;
|
|
||||||
this.selectedFiles.add(filePath);
|
|
||||||
console.log('Added file to selection, count:', this.selectedFiles.size);
|
|
||||||
this._onDidChangeTreeData.fire();
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFromSelection(item: FileTreeItem): void {
|
|
||||||
const filePath = item.resourceUri.fsPath;
|
|
||||||
this.selectedFiles.delete(filePath);
|
|
||||||
console.log('Removed file from selection, count:', this.selectedFiles.size);
|
|
||||||
this._onDidChangeTreeData.fire();
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedFiles(): Set<string> {
|
|
||||||
console.log('getSelectedFiles called, count:', this.selectedFiles.size);
|
|
||||||
return this.selectedFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleXmlEdits(): void {
|
|
||||||
this.xmlEditsEnabled = !this.xmlEditsEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
isXmlEditsEnabled(): boolean {
|
|
||||||
return this.xmlEditsEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh(): void {
|
|
||||||
this._onDidChangeTreeData.fire();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility function to estimate tokens in text
|
|
||||||
function estimateTokens(text: string): number {
|
|
||||||
// Rough estimation: Split by whitespace and punctuation
|
|
||||||
// This is a simple approximation, actual token count may vary by model
|
|
||||||
const words = text.split(/[\s\p{P}]+/u).filter(Boolean);
|
|
||||||
return Math.ceil(words.length * 1.3); // Add 30% overhead for special tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to read file content
|
|
||||||
async function readFileContent(filePath: string): Promise<string> {
|
|
||||||
try {
|
|
||||||
const readData = await vscode.workspace.fs.readFile(vscode.Uri.file(filePath));
|
|
||||||
return Buffer.from(readData).toString('utf8');
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error reading file ${filePath}:`, error);
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to generate XML prompt
|
|
||||||
function generateXMLPrompt(files: Map<string, { content: string; tokens: number }>, xmlEdits: boolean): string {
|
|
||||||
const xmlParts = ['<?xml version="1.0" encoding="UTF-8"?>', '<prompt>'];
|
|
||||||
|
|
||||||
// Add files section
|
|
||||||
xmlParts.push(' <files>');
|
|
||||||
for (const [path, { content, tokens }] of files) {
|
|
||||||
xmlParts.push(' <file>');
|
|
||||||
xmlParts.push(` <path>${path}</path>`);
|
|
||||||
xmlParts.push(` <tokens>${tokens}</tokens>`);
|
|
||||||
xmlParts.push(` <content><![CDATA[${content}]]></content>`);
|
|
||||||
xmlParts.push(' </file>');
|
|
||||||
}
|
|
||||||
xmlParts.push(' </files>');
|
|
||||||
|
|
||||||
// Add XML edits flag if enabled
|
|
||||||
if (xmlEdits) {
|
|
||||||
xmlParts.push(' <options>');
|
|
||||||
xmlParts.push(' <xml_edits>true</xml_edits>');
|
|
||||||
xmlParts.push(' </options>');
|
|
||||||
}
|
|
||||||
|
|
||||||
xmlParts.push('</prompt>');
|
|
||||||
return xmlParts.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// This method is called when your extension is activated
|
|
||||||
export function activate(context: vscode.ExtensionContext) {
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
console.log('Congratulations, your extension "prompter" is now active!');
|
console.log('Prompter extension is now active!');
|
||||||
|
|
||||||
|
// Get the workspace folder
|
||||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath;
|
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath;
|
||||||
|
console.log('Workspace root:', workspaceRoot);
|
||||||
|
|
||||||
|
// Create the tree data provider
|
||||||
|
console.log('Creating tree data provider...');
|
||||||
const prompterTreeProvider = new PrompterTreeProvider(workspaceRoot);
|
const prompterTreeProvider = new PrompterTreeProvider(workspaceRoot);
|
||||||
|
console.log('Tree data provider created successfully');
|
||||||
|
|
||||||
// Register the TreeView with checkbox support
|
// Register the TreeView with checkbox support
|
||||||
const treeView = vscode.window.createTreeView('prompterView', {
|
console.log('Registering tree view with ID: prompterView');
|
||||||
treeDataProvider: prompterTreeProvider,
|
let treeView: vscode.TreeView<FileTreeItem | SettingTreeItem>;
|
||||||
showCollapseAll: true,
|
try {
|
||||||
canSelectMany: true
|
treeView = vscode.window.createTreeView('prompterView', {
|
||||||
});
|
treeDataProvider: prompterTreeProvider,
|
||||||
|
showCollapseAll: true,
|
||||||
|
canSelectMany: true
|
||||||
|
});
|
||||||
|
console.log('Tree view registered successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error registering tree view:', error);
|
||||||
|
// Create a fallback empty tree view to prevent further errors
|
||||||
|
treeView = {} as vscode.TreeView<FileTreeItem | SettingTreeItem>;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle checkbox changes
|
// Handle checkbox changes
|
||||||
treeView.onDidChangeCheckboxState(e => {
|
treeView.onDidChangeCheckboxState((e: vscode.TreeCheckboxChangeEvent<FileTreeItem | SettingTreeItem>) => {
|
||||||
console.log('Checkbox state changed');
|
console.log('Checkbox state changed');
|
||||||
for (const [item, state] of e.items) {
|
for (const [item, state] of e.items) {
|
||||||
const fileItem = item as FileTreeItem;
|
if (item instanceof FileTreeItem) {
|
||||||
console.log(`Checkbox changed for ${fileItem.resourceUri.fsPath} to ${state}`);
|
console.log(`Checkbox changed for ${item.resourceUri.fsPath} to ${state}`);
|
||||||
if (state === vscode.TreeItemCheckboxState.Checked) {
|
if (state === vscode.TreeItemCheckboxState.Checked) {
|
||||||
prompterTreeProvider.addToSelection(fileItem);
|
prompterTreeProvider.addToSelection(item);
|
||||||
} else {
|
} else {
|
||||||
prompterTreeProvider.removeFromSelection(fileItem);
|
prompterTreeProvider.removeFromSelection(item);
|
||||||
|
}
|
||||||
|
} else if (item instanceof SettingTreeItem) {
|
||||||
|
// Handle settings checkbox changes
|
||||||
|
console.log(`Setting changed: ${item.settingKey} to ${state === vscode.TreeItemCheckboxState.Checked}`);
|
||||||
|
prompterTreeProvider.updateSetting(
|
||||||
|
item.settingKey,
|
||||||
|
state === vscode.TreeItemCheckboxState.Checked
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update formatting instructions button text if that setting changed
|
||||||
|
if (item.settingKey === 'includeFormattingInstructions') {
|
||||||
|
xmlEditsButton.text = prompterTreeProvider.isXmlEditsEnabled() ?
|
||||||
|
"$(check) Formatting Instructions" : "$(diff-added) XML Edits";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create XML edits toggle button
|
// Create formatting instructions toggle button
|
||||||
const xmlEditsButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
|
const xmlEditsButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1);
|
||||||
xmlEditsButton.text = "$(diff-added) XML Edits";
|
xmlEditsButton.text = "$(diff-added) XML Edits";
|
||||||
xmlEditsButton.tooltip = "Toggle XML Edits mode";
|
xmlEditsButton.tooltip = "Toggle formatting instructions mode";
|
||||||
xmlEditsButton.command = 'prompter.toggleXmlEdits';
|
xmlEditsButton.command = 'prompter.toggleXmlEdits';
|
||||||
xmlEditsButton.show();
|
xmlEditsButton.show();
|
||||||
|
|
||||||
// Create copy button
|
// Create copy button
|
||||||
const copyButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right);
|
const copyButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 3);
|
||||||
copyButton.text = "$(clippy) Copy";
|
copyButton.text = "$(clippy) Copy";
|
||||||
copyButton.tooltip = "Generate and copy XML prompt";
|
copyButton.tooltip = "Generate and copy prompt";
|
||||||
copyButton.command = 'prompter.generatePrompt';
|
copyButton.command = 'prompter.generatePrompt';
|
||||||
copyButton.show();
|
copyButton.show();
|
||||||
|
|
||||||
|
// Create settings button
|
||||||
|
const settingsButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 2);
|
||||||
|
settingsButton.text = "$(settings-gear)";
|
||||||
|
settingsButton.tooltip = "Prompter Settings";
|
||||||
|
settingsButton.command = 'prompter.openSettings';
|
||||||
|
settingsButton.show();
|
||||||
|
|
||||||
// Register command to toggle file selection
|
// Register command to toggle file selection
|
||||||
let toggleSelectionCommand = vscode.commands.registerCommand('prompter.toggleSelection', (item: FileTreeItem) => {
|
let toggleSelectionCommand = vscode.commands.registerCommand('prompter.toggleSelection', (item: FileTreeItem) => {
|
||||||
console.log('Toggle selection command triggered for:', item.resourceUri.fsPath);
|
if (item.resourceUri) {
|
||||||
prompterTreeProvider.toggleSelection(item);
|
console.log('Toggle selection command triggered for:', item.resourceUri.fsPath);
|
||||||
|
prompterTreeProvider.toggleSelection(item);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register command to toggle XML edits
|
// Register command to toggle XML edits
|
||||||
@@ -213,54 +101,73 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
xmlEditsButton.text = prompterTreeProvider.isXmlEditsEnabled() ?
|
xmlEditsButton.text = prompterTreeProvider.isXmlEditsEnabled() ?
|
||||||
"$(check) XML Edits" : "$(diff-added) XML Edits";
|
"$(check) XML Edits" : "$(diff-added) XML Edits";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Register command to open settings
|
||||||
|
let openSettingsCommand = vscode.commands.registerCommand('prompter.openSettings', () => {
|
||||||
|
prompterTreeProvider.toggleSettingsView();
|
||||||
|
|
||||||
|
// Update the settings button icon based on current view
|
||||||
|
settingsButton.text = prompterTreeProvider.isShowingSettings() ?
|
||||||
|
"$(list-tree)" : "$(settings-gear)";
|
||||||
|
settingsButton.tooltip = prompterTreeProvider.isShowingSettings() ?
|
||||||
|
"Show Files" : "Prompter Settings";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register command to show the tree view
|
||||||
|
let openTreeViewCommand = vscode.commands.registerCommand('prompter.openTreeView', () => {
|
||||||
|
prompterTreeProvider.showFilesView();
|
||||||
|
});
|
||||||
|
|
||||||
// Register command to generate prompt from selected files
|
// Register command to generate prompt from selected files
|
||||||
let generatePromptCommand = vscode.commands.registerCommand('prompter.generatePrompt', async () => {
|
let generatePromptCommand = vscode.commands.registerCommand('prompter.generatePrompt', async () => {
|
||||||
const selectedFiles = prompterTreeProvider.getSelectedFiles();
|
const selectedFiles = prompterTreeProvider.getSelectedFiles();
|
||||||
console.log('Generate prompt command triggered, selected files:', [...selectedFiles]);
|
console.log('Generate prompt command triggered, selected files:', [...selectedFiles]);
|
||||||
|
|
||||||
if (selectedFiles.size === 0) {
|
if (selectedFiles.size === 0) {
|
||||||
vscode.window.showInformationMessage('Please select files first');
|
vscode.window.showInformationMessage('Please select files first');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalTokens = 0;
|
try {
|
||||||
const fileContents = new Map<string, { content: string; tokens: number }>();
|
// Generate the prompt using our utility class
|
||||||
|
const promptText = await PromptGenerator.generatePrompt(
|
||||||
// Process each selected file
|
selectedFiles,
|
||||||
for (const filePath of selectedFiles) {
|
prompterTreeProvider.getSettings()
|
||||||
const content = await readFileContent(filePath);
|
|
||||||
const tokens = estimateTokens(content);
|
|
||||||
totalTokens += tokens;
|
|
||||||
fileContents.set(filePath, { content, tokens });
|
|
||||||
|
|
||||||
// Show individual file token count
|
|
||||||
vscode.window.showInformationMessage(
|
|
||||||
`File: ${filePath}\nEstimated tokens: ${tokens}`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check if we got a valid prompt text
|
||||||
|
if (promptText === null) {
|
||||||
|
// Show warning if all files were filtered out
|
||||||
|
vscode.window.showWarningMessage('All selected files were filtered out by ignore patterns');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy to clipboard only if we have valid content
|
||||||
|
await vscode.env.clipboard.writeText(promptText);
|
||||||
|
vscode.window.showInformationMessage('Prompt copied to clipboard!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating prompt:', error);
|
||||||
|
vscode.window.showErrorMessage('Error generating prompt');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show total token count
|
|
||||||
vscode.window.showInformationMessage(
|
|
||||||
`Total estimated tokens for ${selectedFiles.size} files: ${totalTokens}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Generate XML prompt
|
|
||||||
const xmlPrompt = generateXMLPrompt(fileContents, prompterTreeProvider.isXmlEditsEnabled());
|
|
||||||
|
|
||||||
// Copy to clipboard
|
|
||||||
await vscode.env.clipboard.writeText(xmlPrompt);
|
|
||||||
vscode.window.showInformationMessage('XML prompt copied to clipboard!');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add all disposables to context subscriptions
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
treeView,
|
treeView,
|
||||||
xmlEditsButton,
|
xmlEditsButton,
|
||||||
copyButton,
|
copyButton,
|
||||||
|
settingsButton,
|
||||||
toggleSelectionCommand,
|
toggleSelectionCommand,
|
||||||
toggleXmlEditsCommand,
|
toggleXmlEditsCommand,
|
||||||
generatePromptCommand
|
generatePromptCommand,
|
||||||
|
openSettingsCommand,
|
||||||
|
openTreeViewCommand
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method is called when your extension is deactivated
|
/**
|
||||||
export function deactivate() {}
|
* This method is called when the extension is deactivated
|
||||||
|
*/
|
||||||
|
export function deactivate() {
|
||||||
|
// Clean up resources when the extension is deactivated
|
||||||
|
}
|
||||||
|
|||||||
13
src/models/settings.ts
Normal file
13
src/models/settings.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Settings interface for the Prompter extension
|
||||||
|
export interface PrompterSettings {
|
||||||
|
includeFormattingInstructions: boolean;
|
||||||
|
tokenCalculationEnabled: boolean;
|
||||||
|
includeFileMap: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default settings values
|
||||||
|
export const DEFAULT_SETTINGS: PrompterSettings = {
|
||||||
|
includeFormattingInstructions: false,
|
||||||
|
tokenCalculationEnabled: true,
|
||||||
|
includeFileMap: true
|
||||||
|
};
|
||||||
32
src/models/treeItems.ts
Normal file
32
src/models/treeItems.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { PrompterSettings } from './settings';
|
||||||
|
|
||||||
|
// Tree item for files in the explorer
|
||||||
|
export class FileTreeItem extends vscode.TreeItem {
|
||||||
|
constructor(
|
||||||
|
public readonly resourceUri: vscode.Uri,
|
||||||
|
public readonly collapsibleState: vscode.TreeItemCollapsibleState
|
||||||
|
) {
|
||||||
|
super(resourceUri, collapsibleState);
|
||||||
|
this.tooltip = resourceUri.fsPath;
|
||||||
|
this.description = path.basename(resourceUri.fsPath);
|
||||||
|
this.contextValue = 'file';
|
||||||
|
|
||||||
|
// Set the checkbox state to unchecked by default
|
||||||
|
this.checkboxState = vscode.TreeItemCheckboxState.Unchecked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tree item for settings in the settings view
|
||||||
|
export class SettingTreeItem extends vscode.TreeItem {
|
||||||
|
constructor(
|
||||||
|
public readonly label: string,
|
||||||
|
public readonly settingKey: keyof PrompterSettings,
|
||||||
|
public readonly checked: boolean
|
||||||
|
) {
|
||||||
|
super(label, vscode.TreeItemCollapsibleState.None);
|
||||||
|
this.checkboxState = checked ? vscode.TreeItemCheckboxState.Checked : vscode.TreeItemCheckboxState.Unchecked;
|
||||||
|
this.contextValue = 'setting';
|
||||||
|
}
|
||||||
|
}
|
||||||
209
src/providers/fileSelectionManager.ts
Normal file
209
src/providers/fileSelectionManager.ts
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
// Use require for ignore package with proper fallback
|
||||||
|
let ignoreFunc: () => any;
|
||||||
|
try {
|
||||||
|
const ignoreModule = require('ignore');
|
||||||
|
if (typeof ignoreModule === 'function') {
|
||||||
|
ignoreFunc = ignoreModule;
|
||||||
|
} else if (ignoreModule && typeof ignoreModule.default === 'function') {
|
||||||
|
ignoreFunc = ignoreModule.default;
|
||||||
|
} else {
|
||||||
|
throw new Error('Ignore module is neither a function nor has a default function');
|
||||||
|
}
|
||||||
|
console.log('Successfully loaded ignore function in FileSelectionManager');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading ignore package in FileSelectionManager:', error);
|
||||||
|
ignoreFunc = () => ({
|
||||||
|
add: () => {},
|
||||||
|
ignores: () => false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages file selection state for the Prompter extension
|
||||||
|
*/
|
||||||
|
export class FileSelectionManager {
|
||||||
|
private selectedFiles: Set<string> = new Set();
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path relative to the workspace root
|
||||||
|
* @param filePath Absolute file path
|
||||||
|
* @param rootPath Workspace root path
|
||||||
|
* @returns Relative path
|
||||||
|
*/
|
||||||
|
private getRelativePath(filePath: string, rootPath: string): string {
|
||||||
|
if (filePath.startsWith(rootPath)) {
|
||||||
|
const relativePath = filePath.substring(rootPath.length);
|
||||||
|
return relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
|
||||||
|
}
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a path or any of its parent directories are ignored
|
||||||
|
* @param pathToCheck The path to check
|
||||||
|
* @param ig The ignore instance
|
||||||
|
* @param workspaceRoot The workspace root path
|
||||||
|
* @returns True if the path or any parent is ignored
|
||||||
|
*/
|
||||||
|
private isPathIgnored(pathToCheck: string, ig: any, workspaceRoot: string): boolean {
|
||||||
|
let currentPath = pathToCheck;
|
||||||
|
while (currentPath !== workspaceRoot) {
|
||||||
|
const relativePath = this.getRelativePath(currentPath, workspaceRoot);
|
||||||
|
const normalizedPath = relativePath.split(path.sep).join('/');
|
||||||
|
const isIgnored = ig.ignores(normalizedPath);
|
||||||
|
console.log(`Checking ${normalizedPath}: ignored = ${isIgnored}`);
|
||||||
|
if (isIgnored) {
|
||||||
|
console.log(`Path ${pathToCheck} ignored because parent ${normalizedPath} is ignored`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
currentPath = path.dirname(currentPath);
|
||||||
|
if (currentPath === workspaceRoot) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load ignore patterns from .gitignore files in the given directory and its parent directories
|
||||||
|
* @param directoryPath The directory path to start searching from
|
||||||
|
* @param workspaceRoot The workspace root path
|
||||||
|
* @returns An ignore instance with loaded patterns
|
||||||
|
*/
|
||||||
|
private loadIgnorePatternsFromDirectory(directoryPath: string, workspaceRoot: string): any {
|
||||||
|
const ig = ignoreFunc();
|
||||||
|
let currentDir = directoryPath;
|
||||||
|
|
||||||
|
// Check for .gitignore in the current directory and all parent directories up to workspace root
|
||||||
|
while (currentDir.startsWith(workspaceRoot)) {
|
||||||
|
try {
|
||||||
|
const gitignorePath = path.join(currentDir, '.gitignore');
|
||||||
|
if (fs.existsSync(gitignorePath)) {
|
||||||
|
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
||||||
|
ig.add(gitignoreContent);
|
||||||
|
console.log(`Loaded .gitignore patterns from ${gitignorePath}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error loading .gitignore from ${currentDir}:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop if we've reached the workspace root
|
||||||
|
if (currentDir === workspaceRoot) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move up to parent directory
|
||||||
|
const parentDir = path.dirname(currentDir);
|
||||||
|
if (parentDir === currentDir) { // Avoid infinite loop if we've reached the root
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentDir = parentDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a file to the selection if it's not ignored
|
||||||
|
*/
|
||||||
|
addFile(filePath: string): void {
|
||||||
|
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || '';
|
||||||
|
const dirPath = path.dirname(filePath);
|
||||||
|
const ig = this.loadIgnorePatternsFromDirectory(dirPath, workspaceRoot);
|
||||||
|
|
||||||
|
if (this.isPathIgnored(filePath, ig, workspaceRoot)) {
|
||||||
|
console.log(`Ignoring file ${filePath} because it or a parent directory is ignored`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.selectedFiles.add(filePath);
|
||||||
|
console.log(`Added ${filePath} to selection`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a directory and all its non-ignored contents to the selection
|
||||||
|
*/
|
||||||
|
async addDirectory(dirPath: string): Promise<void> {
|
||||||
|
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || '';
|
||||||
|
const ig = this.loadIgnorePatternsFromDirectory(dirPath, workspaceRoot);
|
||||||
|
|
||||||
|
const relativeDirPath = this.getRelativePath(dirPath, workspaceRoot);
|
||||||
|
const normalizedDirPath = relativeDirPath.split(path.sep).join('/');
|
||||||
|
if (ig.ignores(normalizedDirPath)) {
|
||||||
|
console.log(`Directory ${dirPath} is ignored, skipping its contents`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedFiles.add(dirPath);
|
||||||
|
console.log(`Added directory ${dirPath} to selection`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath));
|
||||||
|
for (const [name, type] of files) {
|
||||||
|
const filePath = path.join(dirPath, name);
|
||||||
|
const relativeFilePath = this.getRelativePath(filePath, workspaceRoot);
|
||||||
|
const normalizedFilePath = relativeFilePath.split(path.sep).join('/');
|
||||||
|
if (type === vscode.FileType.Directory) {
|
||||||
|
await this.addDirectory(filePath);
|
||||||
|
} else if (!ig.ignores(normalizedFilePath)) {
|
||||||
|
this.selectedFiles.add(filePath);
|
||||||
|
console.log(`Added ${filePath} to selection`);
|
||||||
|
} else {
|
||||||
|
console.log(`Ignoring ${filePath} due to ignore patterns`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error adding directory ${dirPath}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a file from the selection
|
||||||
|
*/
|
||||||
|
removeFile(filePath: string): void {
|
||||||
|
this.selectedFiles.delete(filePath);
|
||||||
|
console.log(`Removed ${filePath} from selection`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a directory and all its contents from the selection
|
||||||
|
*/
|
||||||
|
async removeDirectory(dirPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.selectedFiles.delete(dirPath);
|
||||||
|
console.log(`Removed directory ${dirPath} from selection`);
|
||||||
|
const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath));
|
||||||
|
for (const [name, type] of files) {
|
||||||
|
const filePath = path.join(dirPath, name);
|
||||||
|
if (type === vscode.FileType.Directory) {
|
||||||
|
await this.removeDirectory(filePath);
|
||||||
|
} else {
|
||||||
|
this.selectedFiles.delete(filePath);
|
||||||
|
console.log(`Removed ${filePath} from selection (from directory)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error removing directory from selection: ${dirPath}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file is selected
|
||||||
|
*/
|
||||||
|
isSelected(filePath: string): boolean {
|
||||||
|
return this.selectedFiles.has(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all selected files
|
||||||
|
*/
|
||||||
|
getSelectedFiles(): Set<string> {
|
||||||
|
console.log('getSelectedFiles called, count:', this.selectedFiles.size);
|
||||||
|
return this.selectedFiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
211
src/providers/prompterTreeProvider.ts
Normal file
211
src/providers/prompterTreeProvider.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { FileTreeItem, SettingTreeItem } from '../models/treeItems';
|
||||||
|
import { PrompterSettings } from '../models/settings';
|
||||||
|
import { FileSelectionManager } from './fileSelectionManager';
|
||||||
|
import { SettingsManager } from './settingsManager';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tree provider for the Prompter extension
|
||||||
|
* Handles both file browsing and settings views
|
||||||
|
*/
|
||||||
|
export class PrompterTreeProvider implements vscode.TreeDataProvider<FileTreeItem | SettingTreeItem> {
|
||||||
|
private _onDidChangeTreeData: vscode.EventEmitter<FileTreeItem | SettingTreeItem | undefined | null | void> =
|
||||||
|
new vscode.EventEmitter<FileTreeItem | SettingTreeItem | undefined | null | void>();
|
||||||
|
|
||||||
|
readonly onDidChangeTreeData: vscode.Event<FileTreeItem | SettingTreeItem | undefined | null | void> =
|
||||||
|
this._onDidChangeTreeData.event;
|
||||||
|
|
||||||
|
private fileSelectionManager: FileSelectionManager;
|
||||||
|
private settingsManager: SettingsManager;
|
||||||
|
private showingSettings: boolean = false;
|
||||||
|
|
||||||
|
constructor(private workspaceRoot: string | undefined) {
|
||||||
|
this.fileSelectionManager = new FileSelectionManager();
|
||||||
|
this.settingsManager = new SettingsManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle between file view and settings view
|
||||||
|
toggleSettingsView(): void {
|
||||||
|
this.showingSettings = !this.showingSettings;
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
showSettingsView(): void {
|
||||||
|
this.showingSettings = true;
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
showFilesView(): void {
|
||||||
|
this.showingSettings = false;
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTreeItem(element: FileTreeItem | SettingTreeItem): vscode.TreeItem {
|
||||||
|
// Return the element as is if it's a SettingTreeItem
|
||||||
|
if (element instanceof SettingTreeItem) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle FileTreeItem
|
||||||
|
const fileItem = element as FileTreeItem;
|
||||||
|
if (fileItem.resourceUri && this.fileSelectionManager.isSelected(fileItem.resourceUri.fsPath)) {
|
||||||
|
fileItem.checkboxState = vscode.TreeItemCheckboxState.Checked;
|
||||||
|
} else {
|
||||||
|
fileItem.checkboxState = vscode.TreeItemCheckboxState.Unchecked;
|
||||||
|
}
|
||||||
|
return fileItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChildren(element?: FileTreeItem | SettingTreeItem): Promise<(FileTreeItem | SettingTreeItem)[]> {
|
||||||
|
// If we're showing settings, return settings items
|
||||||
|
if (this.showingSettings && !element) {
|
||||||
|
return this.getSettingsItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise show files
|
||||||
|
if (!this.workspaceRoot) {
|
||||||
|
vscode.window.showInformationMessage('No workspace folder is opened');
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
// Make sure we're dealing with a FileTreeItem that has a resourceUri
|
||||||
|
if (!(element instanceof SettingTreeItem) && element.resourceUri) {
|
||||||
|
return this.getFilesInDirectory(element.resourceUri.fsPath);
|
||||||
|
}
|
||||||
|
return Promise.resolve([]);
|
||||||
|
} else {
|
||||||
|
return this.getFilesInDirectory(this.workspaceRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getFilesInDirectory(dirPath: string): Promise<FileTreeItem[]> {
|
||||||
|
try {
|
||||||
|
const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath));
|
||||||
|
|
||||||
|
// Sort by type first (directories first), then alphabetically by name
|
||||||
|
const sortedFiles = [...files].sort((a, b) => {
|
||||||
|
const [nameA, typeA] = a;
|
||||||
|
const [nameB, typeB] = b;
|
||||||
|
|
||||||
|
// If types are different, directories (type 2) come before files (type 1)
|
||||||
|
if (typeA !== typeB) {
|
||||||
|
return typeB - typeA; // Descending order by type puts directories first
|
||||||
|
}
|
||||||
|
|
||||||
|
// If types are the same, sort alphabetically by name
|
||||||
|
return nameA.localeCompare(nameB, undefined, { sensitivity: 'base' });
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortedFiles.map(([name, type]) => {
|
||||||
|
const filePath = path.join(dirPath, name);
|
||||||
|
const uri = vscode.Uri.file(filePath);
|
||||||
|
|
||||||
|
return new FileTreeItem(
|
||||||
|
uri,
|
||||||
|
type === vscode.FileType.Directory
|
||||||
|
? vscode.TreeItemCollapsibleState.Collapsed
|
||||||
|
: vscode.TreeItemCollapsibleState.None
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error reading directory: ${dirPath}`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the tree view
|
||||||
|
refresh(): void {
|
||||||
|
this._onDidChangeTreeData.fire();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a file to the selection
|
||||||
|
addToSelection(item: FileTreeItem): void {
|
||||||
|
if (item.resourceUri) {
|
||||||
|
if (this.isDirectory(item)) {
|
||||||
|
this.fileSelectionManager.addDirectory(item.resourceUri.fsPath);
|
||||||
|
} else {
|
||||||
|
this.fileSelectionManager.addFile(item.resourceUri.fsPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a file from the selection
|
||||||
|
removeFromSelection(item: FileTreeItem): void {
|
||||||
|
if (item.resourceUri) {
|
||||||
|
if (this.isDirectory(item)) {
|
||||||
|
this.fileSelectionManager.removeDirectory(item.resourceUri.fsPath);
|
||||||
|
} else {
|
||||||
|
this.fileSelectionManager.removeFile(item.resourceUri.fsPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isDirectory(item: FileTreeItem): boolean {
|
||||||
|
return item.collapsibleState === vscode.TreeItemCollapsibleState.Collapsed ||
|
||||||
|
item.collapsibleState === vscode.TreeItemCollapsibleState.Expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle a file's selection status
|
||||||
|
toggleSelection(item: FileTreeItem): void {
|
||||||
|
if (item.resourceUri) {
|
||||||
|
const filePath = item.resourceUri.fsPath;
|
||||||
|
if (this.fileSelectionManager.isSelected(filePath)) {
|
||||||
|
this.removeFromSelection(item);
|
||||||
|
} else {
|
||||||
|
this.addToSelection(item);
|
||||||
|
}
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the selected files
|
||||||
|
getSelectedFiles(): Set<string> {
|
||||||
|
return this.fileSelectionManager.getSelectedFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get settings items for the tree view
|
||||||
|
private getSettingsItems(): SettingTreeItem[] {
|
||||||
|
const settings = this.settingsManager.getSettings();
|
||||||
|
return [
|
||||||
|
new SettingTreeItem('Include Formatting Instructions', 'includeFormattingInstructions', settings.includeFormattingInstructions),
|
||||||
|
new SettingTreeItem('Token Calculation', 'tokenCalculationEnabled', settings.tokenCalculationEnabled),
|
||||||
|
new SettingTreeItem('Include File Map', 'includeFileMap', settings.includeFileMap)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update a setting value
|
||||||
|
updateSetting(key: keyof PrompterSettings, value: boolean): void {
|
||||||
|
this.settingsManager.updateSetting(key, value);
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if showing settings view
|
||||||
|
isShowingSettings(): boolean {
|
||||||
|
return this.showingSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle formatting instructions setting
|
||||||
|
toggleXmlEdits(): void {
|
||||||
|
this.settingsManager.toggleFormattingInstructions();
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if formatting instructions are enabled
|
||||||
|
isXmlEditsEnabled(): boolean {
|
||||||
|
return this.settingsManager.isFormattingInstructionsEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all settings
|
||||||
|
getSettings(): PrompterSettings {
|
||||||
|
return this.settingsManager.getSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all settings at once
|
||||||
|
updateSettings(newSettings: PrompterSettings): void {
|
||||||
|
this.settingsManager.updateAllSettings(newSettings);
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/providers/settingsManager.ts
Normal file
46
src/providers/settingsManager.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
import { PrompterSettings, DEFAULT_SETTINGS } from '../models/settings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages settings for the Prompter extension
|
||||||
|
*/
|
||||||
|
export class SettingsManager {
|
||||||
|
private settings: PrompterSettings = { ...DEFAULT_SETTINGS };
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a specific setting
|
||||||
|
*/
|
||||||
|
updateSetting(key: keyof PrompterSettings, value: boolean): void {
|
||||||
|
this.settings[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle formatting instructions setting
|
||||||
|
*/
|
||||||
|
toggleFormattingInstructions(): void {
|
||||||
|
this.settings.includeFormattingInstructions = !this.settings.includeFormattingInstructions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if formatting instructions are enabled
|
||||||
|
*/
|
||||||
|
isFormattingInstructionsEnabled(): boolean {
|
||||||
|
return this.settings.includeFormattingInstructions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all settings
|
||||||
|
*/
|
||||||
|
getSettings(): PrompterSettings {
|
||||||
|
return { ...this.settings };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update all settings at once
|
||||||
|
*/
|
||||||
|
updateAllSettings(newSettings: PrompterSettings): void {
|
||||||
|
this.settings = { ...newSettings };
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/test/index.ts
Normal file
33
src/test/index.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import Mocha from 'mocha';
|
||||||
|
|
||||||
|
export function run(): Promise<void> {
|
||||||
|
// Create the mocha test
|
||||||
|
const mocha = new Mocha({
|
||||||
|
ui: 'tdd',
|
||||||
|
color: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const testsRoot = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Simple approach - add extension.test.js directly
|
||||||
|
// This will find our basic test file
|
||||||
|
mocha.addFile(path.resolve(testsRoot, 'test/extension.test.js'));
|
||||||
|
|
||||||
|
// Run the mocha test
|
||||||
|
mocha.run((failures: number) => {
|
||||||
|
if (failures > 0) {
|
||||||
|
reject(new Error(`${failures} tests failed.`));
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
59
src/test/runTest.mjs
Normal file
59
src/test/runTest.mjs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { runTests } from '@vscode/test-electron';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
// Create a temporary test workspace
|
||||||
|
// Using the system temp directory instead of external volume
|
||||||
|
const tmpDir = os.tmpdir();
|
||||||
|
const testWorkspaceDir = path.join(tmpDir, `vscode-test-workspace-${Math.random().toString(36).substring(2)}`);
|
||||||
|
const userDataDir = path.join(tmpDir, `vscode-test-user-data-${Math.random().toString(36).substring(2)}`);
|
||||||
|
|
||||||
|
// Ensure the test workspace directory exists
|
||||||
|
if (!fs.existsSync(testWorkspaceDir)) {
|
||||||
|
fs.mkdirSync(testWorkspaceDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// The folder containing the Extension Manifest package.json
|
||||||
|
// Passed to `--extensionDevelopmentPath`
|
||||||
|
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
|
||||||
|
|
||||||
|
// The path to the extension test script
|
||||||
|
// Passed to --extensionTestsPath
|
||||||
|
const extensionTestsPath = path.resolve(__dirname, '../../out/test');
|
||||||
|
|
||||||
|
console.log('Running tests with the following configuration:');
|
||||||
|
console.log(`Extension Development Path: ${extensionDevelopmentPath}`);
|
||||||
|
console.log(`Extension Tests Path: ${extensionTestsPath}`);
|
||||||
|
console.log(`Workspace Dir: ${testWorkspaceDir}`);
|
||||||
|
console.log(`User Data Dir: ${userDataDir}`);
|
||||||
|
|
||||||
|
// Download VS Code, unzip it and run the integration test
|
||||||
|
await runTests({
|
||||||
|
version: '1.98.0', // Specify the exact version your extension is built for
|
||||||
|
extensionDevelopmentPath,
|
||||||
|
extensionTestsPath,
|
||||||
|
launchArgs: [
|
||||||
|
testWorkspaceDir,
|
||||||
|
'--disable-extensions',
|
||||||
|
`--user-data-dir=${userDataDir}`,
|
||||||
|
'--skip-getting-started',
|
||||||
|
'--skip-release-notes',
|
||||||
|
'--disable-telemetry',
|
||||||
|
'--disable-updates',
|
||||||
|
'--disable-crash-reporter',
|
||||||
|
'--disable-workspace-trust'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to run tests', err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
21
src/utils/fileReader.ts
Normal file
21
src/utils/fileReader.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for reading file contents
|
||||||
|
*/
|
||||||
|
export class FileReader {
|
||||||
|
/**
|
||||||
|
* Read the content of a file
|
||||||
|
* @param filePath Path to the file to read
|
||||||
|
* @returns The file content as a string
|
||||||
|
*/
|
||||||
|
static async readFileContent(filePath: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const readData = await vscode.workspace.fs.readFile(vscode.Uri.file(filePath));
|
||||||
|
return Buffer.from(readData).toString('utf8');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error reading file ${filePath}:`, error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
291
src/utils/promptGenerator.ts
Normal file
291
src/utils/promptGenerator.ts
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { PrompterSettings } from '../models/settings';
|
||||||
|
import { FileReader } from './fileReader';
|
||||||
|
import { TokenEstimator } from './tokenEstimator';
|
||||||
|
|
||||||
|
// Use require for ignore package with proper fallback
|
||||||
|
let ignoreFunc: () => any;
|
||||||
|
try {
|
||||||
|
const ignoreModule = require('ignore');
|
||||||
|
if (typeof ignoreModule === 'function') {
|
||||||
|
ignoreFunc = ignoreModule;
|
||||||
|
} else if (ignoreModule && typeof ignoreModule.default === 'function') {
|
||||||
|
ignoreFunc = ignoreModule.default;
|
||||||
|
} else {
|
||||||
|
throw new Error('Ignore module is neither a function nor has a default function');
|
||||||
|
}
|
||||||
|
console.log('Successfully loaded ignore function');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading ignore package:', error);
|
||||||
|
ignoreFunc = () => ({
|
||||||
|
add: () => {},
|
||||||
|
ignores: () => false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PromptGenerator {
|
||||||
|
/**
|
||||||
|
* Generate a prompt from the selected files
|
||||||
|
* @param selectedFiles Set of file paths to include in the prompt
|
||||||
|
* @param settings Settings to apply when generating the prompt
|
||||||
|
* @returns The generated prompt text
|
||||||
|
*/
|
||||||
|
static async generatePrompt(selectedFiles: Set<string>, settings: PrompterSettings): Promise<string | null> {
|
||||||
|
if (selectedFiles.size === 0) {
|
||||||
|
throw new Error('No files selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || '';
|
||||||
|
|
||||||
|
// Create ignore instance using the resolved function
|
||||||
|
const ig = ignoreFunc();
|
||||||
|
console.log('Created ignore instance');
|
||||||
|
|
||||||
|
// Load .gitignore patterns
|
||||||
|
try {
|
||||||
|
const gitignorePath = path.join(workspaceRoot, '.gitignore');
|
||||||
|
if (fs.existsSync(gitignorePath)) {
|
||||||
|
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
||||||
|
ig.add(gitignoreContent);
|
||||||
|
console.log('Successfully loaded .gitignore patterns:', gitignoreContent.split('\n').filter(Boolean));
|
||||||
|
} else {
|
||||||
|
console.log('No .gitignore file found at:', gitignorePath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading .gitignore:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContents = new Map<string, { content: string; tokens: number }>();
|
||||||
|
const filteredFiles = new Set<string>();
|
||||||
|
let totalTokens = 0;
|
||||||
|
|
||||||
|
// Process and filter files
|
||||||
|
for (const filePath of selectedFiles) {
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
console.log(`Skipping directory: ${filePath}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error checking file stats for ${filePath}:`, error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relativePath = this.getRelativePath(filePath, workspaceRoot);
|
||||||
|
const normalizedPath = relativePath.split(path.sep).join('/');
|
||||||
|
|
||||||
|
console.log(`Checking path: ${normalizedPath}`);
|
||||||
|
if (ig.ignores(normalizedPath)) {
|
||||||
|
console.log(`Ignoring file based on patterns: ${normalizedPath}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Processing file: ${normalizedPath}`);
|
||||||
|
filteredFiles.add(filePath);
|
||||||
|
const content = await FileReader.readFileContent(filePath);
|
||||||
|
const tokens = TokenEstimator.estimateTokens(content);
|
||||||
|
totalTokens += tokens;
|
||||||
|
fileContents.set(filePath, { content, tokens });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredFiles.size === 0) {
|
||||||
|
// Return null to signal that no files were available after filtering
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create filtered contents map
|
||||||
|
const filteredContents = new Map<string, { content: string; tokens: number }>();
|
||||||
|
for (const filePath of filteredFiles) {
|
||||||
|
if (fileContents.has(filePath)) {
|
||||||
|
filteredContents.set(filePath, fileContents.get(filePath)!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.generateXMLPrompt(filteredContents, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an XML formatted prompt following the new schema format
|
||||||
|
* @param files Map of file paths to content and token counts
|
||||||
|
* @param settings Settings to apply
|
||||||
|
* @returns XML formatted prompt
|
||||||
|
*/
|
||||||
|
private static generateXMLPrompt(files: Map<string, { content: string; tokens: number }>, settings: PrompterSettings): string {
|
||||||
|
const xmlParts: string[] = [];
|
||||||
|
|
||||||
|
let formattingInstructions = '';
|
||||||
|
if (settings.includeFormattingInstructions) {
|
||||||
|
try {
|
||||||
|
const extensionPath = vscode.extensions.getExtension('prompter')?.extensionPath ||
|
||||||
|
path.join(__dirname, '..', '..');
|
||||||
|
const formattingInstructionsPath = path.join(extensionPath, 'resources', 'xml_formatting_instructions.xml');
|
||||||
|
|
||||||
|
if (fs.existsSync(formattingInstructionsPath)) {
|
||||||
|
formattingInstructions = fs.readFileSync(formattingInstructionsPath, 'utf8');
|
||||||
|
} else {
|
||||||
|
console.warn('XML formatting instructions file not found at:', formattingInstructionsPath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading XML formatting instructions:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.includeFileMap) {
|
||||||
|
xmlParts.push('<file_map>');
|
||||||
|
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || '';
|
||||||
|
xmlParts.push(workspaceRoot);
|
||||||
|
const fileTree = this.generateFileTree(files, workspaceRoot);
|
||||||
|
xmlParts.push(fileTree);
|
||||||
|
xmlParts.push('</file_map>');
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlParts.push('<file_contents>');
|
||||||
|
|
||||||
|
for (const [filePath, { content, tokens }] of files) {
|
||||||
|
const extension = path.extname(filePath);
|
||||||
|
let language = extension.substring(1);
|
||||||
|
|
||||||
|
if (extension === '.js' || extension === '.jsx') {
|
||||||
|
language = 'javascript';
|
||||||
|
} else if (extension === '.ts' || extension === '.tsx') {
|
||||||
|
language = 'typescript';
|
||||||
|
} else if (extension === '.md') {
|
||||||
|
language = 'md';
|
||||||
|
} else if (extension === '.py') {
|
||||||
|
language = 'python';
|
||||||
|
} else if (extension === '.html') {
|
||||||
|
language = 'html';
|
||||||
|
} else if (extension === '.css') {
|
||||||
|
language = 'css';
|
||||||
|
} else if (extension === '.json') {
|
||||||
|
language = 'json';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedContent = content;
|
||||||
|
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || '';
|
||||||
|
|
||||||
|
xmlParts.push(`File: ${this.getRelativePath(filePath, workspaceRoot)}`);
|
||||||
|
xmlParts.push(`\`\`\`${language}`);
|
||||||
|
xmlParts.push(formattedContent);
|
||||||
|
xmlParts.push('\`\`\`');
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlParts.push('</file_contents>');
|
||||||
|
|
||||||
|
if (settings.tokenCalculationEnabled) {
|
||||||
|
const totalTokens = Array.from(files.values()).reduce((sum, { tokens }) => sum + tokens, 0);
|
||||||
|
vscode.window.showInformationMessage(`Total tokens: ${totalTokens}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.includeFormattingInstructions && formattingInstructions) {
|
||||||
|
xmlParts.push(formattingInstructions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return xmlParts.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a tree representation of files
|
||||||
|
* @param files Map of file paths
|
||||||
|
* @param rootPath The workspace root path
|
||||||
|
* @returns String representation of the file tree
|
||||||
|
*/
|
||||||
|
private static generateFileTree(files: Map<string, any>, rootPath: string): string {
|
||||||
|
const treeLines: string[] = [];
|
||||||
|
|
||||||
|
interface TreeNode {
|
||||||
|
name: string;
|
||||||
|
isDirectory: boolean;
|
||||||
|
children: Map<string, TreeNode>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root: TreeNode = {
|
||||||
|
name: path.basename(rootPath),
|
||||||
|
isDirectory: true,
|
||||||
|
children: new Map<string, TreeNode>()
|
||||||
|
};
|
||||||
|
|
||||||
|
const ig = ignoreFunc();
|
||||||
|
try {
|
||||||
|
const gitignorePath = path.join(rootPath, '.gitignore');
|
||||||
|
if (fs.existsSync(gitignorePath)) {
|
||||||
|
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
||||||
|
ig.add(gitignoreContent);
|
||||||
|
ig.add('!.gitignore');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading .gitignore:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const filePath of files.keys()) {
|
||||||
|
const relativePath = this.getRelativePath(filePath, rootPath);
|
||||||
|
const normalizedPath = relativePath.split(path.sep).join('/');
|
||||||
|
|
||||||
|
if (ig.ignores(normalizedPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = relativePath.split('/');
|
||||||
|
let currentNode = root;
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
const part = parts[i];
|
||||||
|
const isDirectory = i < parts.length - 1;
|
||||||
|
|
||||||
|
if (!currentNode.children.has(part)) {
|
||||||
|
currentNode.children.set(part, {
|
||||||
|
name: part,
|
||||||
|
isDirectory,
|
||||||
|
children: new Map<string, TreeNode>()
|
||||||
|
});
|
||||||
|
} else if (isDirectory) {
|
||||||
|
currentNode.children.get(part)!.isDirectory = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentNode = currentNode.children.get(part)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildTreeLines = (node: TreeNode, prefix: string = '', isLast: boolean = true, parentPrefix: string = ''): void => {
|
||||||
|
if (node !== root) {
|
||||||
|
const linePrefix = parentPrefix + (isLast ? '└── ' : '├── ');
|
||||||
|
treeLines.push(`${linePrefix}${node.name}${node.isDirectory ? '' : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedChildren = Array.from(node.children.values())
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.isDirectory === b.isDirectory) {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
}
|
||||||
|
return a.isDirectory ? -1 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
sortedChildren.forEach((child, index) => {
|
||||||
|
const isChildLast = index === sortedChildren.length - 1;
|
||||||
|
const childParentPrefix = node === root ? '' : parentPrefix + (isLast ? ' ' : '│ ');
|
||||||
|
buildTreeLines(child, prefix, isChildLast, childParentPrefix);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
buildTreeLines(root);
|
||||||
|
return treeLines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path relative to the workspace root
|
||||||
|
* @param filePath Absolute file path
|
||||||
|
* @param rootPath Workspace root path
|
||||||
|
* @returns Relative path
|
||||||
|
*/
|
||||||
|
private static getRelativePath(filePath: string, rootPath: string): string {
|
||||||
|
if (filePath.startsWith(rootPath)) {
|
||||||
|
const relativePath = filePath.substring(rootPath.length);
|
||||||
|
return relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
|
||||||
|
}
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/utils/tokenEstimator.ts
Normal file
16
src/utils/tokenEstimator.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Utility for estimating token counts in text
|
||||||
|
*/
|
||||||
|
export class TokenEstimator {
|
||||||
|
/**
|
||||||
|
* Estimate the number of tokens in a text string
|
||||||
|
* @param text The text to estimate tokens for
|
||||||
|
* @returns Estimated token count
|
||||||
|
*/
|
||||||
|
static estimateTokens(text: string): number {
|
||||||
|
// Rough estimation: Split by whitespace and punctuation
|
||||||
|
// This is a simple approximation, actual token count may vary by model
|
||||||
|
const words = text.split(/[\s\p{P}]+/u).filter(Boolean);
|
||||||
|
return Math.ceil(words.length * 1.3); // Add 30% overhead for special tokens
|
||||||
|
}
|
||||||
|
}
|
||||||
18
tsconfig.webpack.json
Normal file
18
tsconfig.webpack.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "ES2020",
|
||||||
|
"outDir": "dist",
|
||||||
|
"sourceMap": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"src/test/**",
|
||||||
|
"**/*.test.ts"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
46
webpack.config.js
Normal file
46
webpack.config.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import('webpack').Configuration}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
target: 'node',
|
||||||
|
mode: 'none', // Set to 'production' for minified output
|
||||||
|
|
||||||
|
entry: {
|
||||||
|
main: './src/extension.ts'
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
filename: 'extension.js',
|
||||||
|
libraryTarget: 'commonjs2',
|
||||||
|
devtoolModuleFilenameTemplate: '../[resource-path]'
|
||||||
|
},
|
||||||
|
devtool: 'source-map',
|
||||||
|
externals: {
|
||||||
|
vscode: 'commonjs vscode' // The vscode module is created on-the-fly and must be excluded
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.js']
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
exclude: /node_modules|src\/test/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'ts-loader',
|
||||||
|
options: {
|
||||||
|
transpileOnly: true,
|
||||||
|
configFile: path.resolve(__dirname, './tsconfig.webpack.json')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
Reference in New Issue
Block a user