VSCode插件开发完整教程:从零开始创建文件导出插件
在日常开发中,我们经常需要将项目文件整理成文档或分享给AI助手进行代码分析。本教程将带你从零开始开发一个VSCode插件,实现右键导出文件夹内容为Markdown文件的功能。
最后实现的插件在应用市场如图,项目是开源的。
🎯 插件功能介绍
我们要开发的插件具有以下特性:
- 在任何文件夹右键菜单中添加"导出为Markdown文件"选项
- 智能过滤不需要的文件(如node_modules、.git等)
- 支持多种编程语言的语法高亮
- 显示文件统计信息和目录结构
- 自定义输出文件名
- 实时进度显示
📋 环境准备
1. 安装必要工具
# 安装Node.js和npm(如果还没有)
# 下载地址:https://nodejs.org/# 安装VSCode插件生成器
npm install -g yo generator-code# 安装插件打包工具(推荐使用本地安装)
npm install -D @vscode/vsce
2. 创建插件项目
# 使用Yeoman生成插件模板
yo code
在交互式界面中选择:
- New Extension (TypeScript) - 选择TypeScript模板
- 插件名称: export-files-extension
- 标识符: export-files-extension
- 描述: 导出文件夹内容到Markdown文件
- 初始化git仓库: Yes
- 安装依赖: Yes
🏗️ 项目结构
生成的项目结构如下:
export-files-extension/
├── src/
│ ├── extension.ts # 主扩展文件
│ ├── exportFiles.ts # 文件导出逻辑
│ └── utils/
│ ├── fileUtils.ts # 文件处理工具
│ └── markdownGen.ts # Markdown生成器
├── package.json # 插件配置
├── tsconfig.json # TypeScript配置
├── .vscodeignore # 打包忽略文件
└── README.md # 说明文档
创建必要的目录和文件:
# 进入项目目录
cd export-files-extension# 创建工具文件夹和文件
mkdir src/utils
touch src/exportFiles.ts
touch src/utils/fileUtils.ts
touch src/utils/markdownGen.ts
⚙️ 配置文件
1. package.json 配置
修改 package.json
文件,添加插件的配置信息:
{"name": "export-files-extension","displayName": "文件导出器","description": "将文件夹内容导出为Markdown文件","version": "1.0.0","engines": {"vscode": "^1.74.0"},"categories": ["Other"],"activationEvents": [],"main": "./out/extension.js","contributes": {"commands": [{"command": "exportFiles.exportToMarkdown","title": "导出为Markdown文件","icon": "$(file-text)"}],"menus": {"explorer/context": [{"command": "exportFiles.exportToMarkdown","when": "explorerResourceIsFolder","group": "7_modification"}]}},"scripts": {"vscode:prepublish": "npm run compile","compile": "tsc -p ./","watch": "tsc -watch -p ./","package": "vsce package --no-dependencies"},"devDependencies": {"@types/vscode": "^1.74.0","@types/node": "16.x","@vscode/vsce": "^2.19.0","typescript": "^4.9.4"}
}
💻 核心代码实现
1. 主扩展文件 (src/extension.ts)
import * as vscode from 'vscode';
import { exportFilesToMarkdown } from './exportFiles';export function activate(context: vscode.ExtensionContext) {console.log('文件导出插件已激活');const disposable = vscode.commands.registerCommand('exportFiles.exportToMarkdown', async (uri: vscode.Uri) => {if (uri && uri.fsPath) {await exportFilesToMarkdown(uri.fsPath);} else {vscode.window.showErrorMessage('请选择一个文件夹');}});context.subscriptions.push(disposable);
}export function deactivate() {}
2. 文件导出逻辑 (src/exportFiles.ts)
import * as vscode from 'vscode';
import * as path from 'path';
import { FileExporter } from './utils/fileUtils';
import { MarkdownGenerator } from './utils/markdownGen';export async function exportFilesToMarkdown(folderPath: string) {try {// 显示输入框让用户选择输出文件名const outputFileName = await vscode.window.showInputBox({prompt: '请输入输出文件名',value: 'project-files.md',validateInput: (value) => {if (!value || !value.trim()) {return '文件名不能为空';}if (!value.endsWith('.md')) {return '文件名必须以.md结尾';}return null;}});if (!outputFileName) {return;}// 显示进度条await vscode.window.withProgress({location: vscode.ProgressLocation.Notification,title: "正在导出文件...",cancellable: false}, async (progress) => {progress.report({ increment: 0 });const fileExporter = new FileExporter(folderPath);const markdownGen = new MarkdownGenerator();progress.report({ increment: 30, message: "扫描文件..." });const files = await fileExporter.getAllFiles();progress.report({ increment: 60, message: "生成Markdown..." });const markdownContent = await markdownGen.generateMarkdown(folderPath, files,outputFileName);progress.report({ increment: 90, message: "保存文件..." });const outputPath = path.join(folderPath, outputFileName);await fileExporter.writeFile(outputPath, markdownContent);progress.report({ increment: 100 });});vscode.window.showInformationMessage(`文件导出成功!保存至: ${outputFileName}`,'打开文件').then(selection => {if (selection === '打开文件') {vscode.workspace.openTextDocument(path.join(folderPath, outputFileName)).then(doc => {vscode.window.showTextDocument(doc);});}});} catch (error) {vscode.window.showErrorMessage(`导出失败: ${error}`);}
}
3. 文件处理工具 (src/utils/fileUtils.ts)
import * as fs from 'fs';
import * as path from 'path';
import { promisify } from 'util';const readdir = promisify(fs.readdir);
const stat = promisify(fs.stat);
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);export interface FileInfo {path: string;relativePath: string;content: string;size: number;extension: string;
}export class FileExporter {private basePath: string;private config = {ignore: ['node_modules', '.git', '.next', 'dist', 'build', 'coverage','.env', '.DS_Store', 'Thumbs.db', '.vercel', '.vscode','yarn.lock', 'package-lock.json', 'pnpm-lock.yaml'],ignoreExtensions: ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.ico', '.webp','.pdf', '.zip', '.rar', '.7z', '.ttf', '.woff', '.woff2','.mp4', '.avi', '.mov', '.mp3', '.wav'],maxFileSize: 1024 * 1024 // 1MB};constructor(basePath: string) {this.basePath = basePath;}async getAllFiles(): Promise<FileInfo[]> {const files: FileInfo[] = [];await this.scanDirectory(this.basePath, files);return files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));}private async scanDirectory(dir: string, files: FileInfo[]): Promise<void> {try {const items = await readdir(dir);for (const item of items) {const fullPath = path.join(dir, item);if (this.shouldIgnore(fullPath)) {continue;}const stats = await stat(fullPath);if (stats.isDirectory()) {await this.scanDirectory(fullPath, files);} else if (stats.isFile()) {if (stats.size > this.config.maxFileSize) {continue;}try {const content = await readFile(fullPath, 'utf8');const relativePath = path.relative(this.basePath, fullPath);files.push({path: fullPath,relativePath,content,size: stats.size,extension: path.extname(fullPath).toLowerCase()});} catch (error) {// 跳过无法读取的文件(如二进制文件)console.log(`跳过文件: ${fullPath}`);}}}} catch (error) {console.error(`无法读取目录: ${dir}`, error);}}private shouldIgnore(filePath: string): boolean {const basename = path.basename(filePath);const ext = path.extname(filePath).toLowerCase();return this.config.ignore.includes(basename) ||this.config.ignoreExtensions.includes(ext) ||filePath.includes('node_modules') ||filePath.includes('.git') ||basename.startsWith('.');}async writeFile(filePath: string, content: string): Promise<void> {await writeFile(filePath, content, 'utf8');}
}
4. Markdown生成器 (src/utils/markdownGen.ts)
import * as path from 'path';
import { FileInfo } from './fileUtils';export class MarkdownGenerator {private getLanguage(filePath: string): string {const ext = path.extname(filePath).toLowerCase();const languageMap: { [key: string]: string } = {'.js': 'javascript','.jsx': 'jsx','.ts': 'typescript','.tsx': 'tsx','.html': 'html','.css': 'css','.scss': 'scss','.sass': 'sass','.less': 'less','.json': 'json','.md': 'markdown','.py': 'python','.java': 'java','.c': 'c','.cpp': 'cpp','.go': 'go','.rs': 'rust','.php': 'php','.sh': 'bash','.sql': 'sql','.xml': 'xml','.yaml': 'yaml','.yml': 'yaml','.vue': 'vue','.svelte': 'svelte'};return languageMap[ext] || 'plaintext';}private formatFileSize(bytes: number): string {if (bytes < 1024) return bytes + ' bytes';if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';return (bytes / (1024 * 1024)).toFixed(1) + ' MB';}async generateMarkdown(basePath: string, files: FileInfo[], outputFileName: string): Promise<string> {let content = `# 项目文件导出\n\n`;content += `导出时间: ${new Date().toLocaleString()}\n\n`;content += `源目录: \`${path.basename(basePath)}\`\n\n`;// 文件统计const totalSize = files.reduce((sum, file) => sum + file.size, 0);const extensionStats: { [key: string]: { count: number; size: number } } = {};files.forEach(file => {const ext = file.extension || '(无扩展名)';if (!extensionStats[ext]) {extensionStats[ext] = { count: 0, size: 0 };}extensionStats[ext].count++;extensionStats[ext].size += file.size;});content += `## 📊 文件统计\n\n`;content += `- 总文件数: ${files.length}\n`;content += `- 总大小: ${this.formatFileSize(totalSize)}\n\n`;// 文件类型分布if (Object.keys(extensionStats).length > 0) {content += `### 📈 文件类型分布\n\n`;content += `| 扩展名 | 文件数 | 总大小 |\n`;content += `| --- | --- | --- |\n`;Object.entries(extensionStats).sort((a, b) => b[1].count - a[1].count).forEach(([ext, stats]) => {content += `| ${ext} | ${stats.count} | ${this.formatFileSize(stats.size)} |\n`;});content += `\n`;}// 文件列表content += `### 📁 文件列表\n\n`;files.forEach(file => {content += `- ${file.relativePath}\n`;});content += `\n`;// 文件内容content += `## 📄 文件内容\n\n`;for (const file of files) {const language = this.getLanguage(file.path);content += `### ${file.relativePath}\n\n`;content += `\`\`\`${language}\n`;content += `// ${file.relativePath}\n`;content += file.content;if (!file.content.endsWith('\n')) {content += '\n';}content += `\`\`\`\n\n`;}return content;}
}
🔧 编译和测试
1. 编译TypeScript
# 编译项目
npm run compile# 或者使用监听模式(推荐开发时使用)
npm run watch
2. 测试插件
- 在VSCode中按
F5
启动扩展开发主机 - 在新窗口中打开一个包含代码文件的项目
- 在资源管理器中右键点击任意文件夹
- 选择"导出为Markdown文件"
- 输入输出文件名(如:project-export.md)
- 等待导出完成,查看生成的Markdown文件
📦 打包插件(重要:解决打包错误)
❌ 常见打包错误
很多开发者在使用传统方法打包时会遇到以下错误:
# 使用全局安装的vsce
npm install -g @vscode/vsce
vsce package# 报错:Command failed: npm list --production --parseable --depth=99999 --loglevel=error
✅ 正确的打包方法
步骤1:本地安装vsce
# 安装到开发依赖(重要:必须加-D)
pnpm i -D @vscode/vsce
步骤2:修改package.json
在 package.json
的 scripts
部分添加:
{"scripts": {"vscode:prepublish": "npm run compile","compile": "tsc -p ./","watch": "tsc -watch -p ./","package": "pnpm vsce package --no-dependencies"}
}
步骤3:执行打包
# 使用npm脚本打包
pnpm run package
🎉 打包成功
如果一切正常,你会看到类似以下输出:
Executing prepublish script 'npm run compile'...
Creating package...
Created: export-files-extension-1.0.0.vsix
🚀 安装和使用
1. 本地安装插件
# 方法1:命令行安装
code --install-extension export-files-extension-1.0.0.vsix# 方法2:通过VSCode界面安装
# 打开VSCode -> 扩展面板 -> 点击"..." -> "从VSIX安装"
2. 使用插件
安装完成后:
- 在VSCode中打开任意项目
- 在资源管理器中右键点击文件夹
- 选择"导出为Markdown文件"
- 输入自定义文件名
- 等待导出完成
- 点击"打开文件"查看结果
🔍 插件特性详解
智能文件过滤
插件会自动忽略以下文件和文件夹:
node_modules
、.git
、dist
、build
等构建目录- 图片、视频、音频等二进制文件
- 环境配置文件(
.env
等) - 超过1MB的大文件
语法高亮支持
支持多种编程语言的语法高亮:
- JavaScript/TypeScript (
.js
,.ts
,.jsx
,.tsx
) - Web技术 (
.html
,.css
,.scss
,.vue
) - 后端语言 (
.py
,.java
,.go
,.php
) - 配置文件 (
.json
,.yaml
,.xml
) - 等等…
详细统计信息
生成的Markdown文件包含:
- 📊 文件总数和总大小
- 📈 按文件类型的分布统计
- 📁 完整的文件列表
- 📄 每个文件的完整内容
🐛 常见问题解决
问题1:编译错误
# 确保TypeScript版本兼容
npm install typescript@^4.9.4 --save-dev
问题2:插件未激活
检查 package.json
中的 activationEvents
配置是否正确。
问题3:右键菜单未显示
确保 contributes.menus
配置正确,特别是 when
条件。
问题4:文件读取失败
检查文件权限,某些系统文件可能无法读取。
🎯 扩展功能建议
你可以进一步扩展插件功能:
- 配置选项:添加用户自定义的忽略规则
- 模板支持:支持不同的导出模板
- 压缩功能:支持导出为ZIP文件
- 云端同步:集成云存储服务
- AI集成:直接发送到AI助手进行分析
📝 总结
通过本教程,我们成功开发了一个功能完整的VSCode插件,实现了:
- ✅ 右键菜单集成
- ✅ 智能文件过滤
- ✅ 多语言语法高亮
- ✅ 详细统计信息
- ✅ 用户友好的界面
- ✅ 完整的错误处理
最重要的是,我们解决了常见的打包问题。记住关键点:
- 本地安装vsce:
npm install -D @vscode/vsce
- 使用–no-dependencies参数:
vsce package --no-dependencies
- 通过npm脚本执行:
npm run package
现在你拥有了一个专业的VSCode插件,可以轻松地将任何项目的代码导出为结构化的Markdown文档,非常适合代码分享、文档生成或AI分析使用!
🔗 相关资源:
- VSCode插件API文档
- vsce打包工具文档
- 插件市场发布指南
希望这个教程对你有帮助!如果在开发过程中遇到问题,欢迎在评论区讨论。