当前位置: 首页 > news >正文

VSCode插件开发完整教程:从零开始创建文件导出插件

在日常开发中,我们经常需要将项目文件整理成文档或分享给AI助手进行代码分析。本教程将带你从零开始开发一个VSCode插件,实现右键导出文件夹内容为Markdown文件的功能。

最后实现的插件在应用市场如图,项目是开源的。code

🎯 插件功能介绍

我们要开发的插件具有以下特性:

  • 在任何文件夹右键菜单中添加"导出为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. 测试插件

  1. 在VSCode中按 F5 启动扩展开发主机
  2. 在新窗口中打开一个包含代码文件的项目
  3. 在资源管理器中右键点击任意文件夹
  4. 选择"导出为Markdown文件"
  5. 输入输出文件名(如:project-export.md)
  6. 等待导出完成,查看生成的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.jsonscripts 部分添加:

{"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. 使用插件

安装完成后:

  1. 在VSCode中打开任意项目
  2. 在资源管理器中右键点击文件夹
  3. 选择"导出为Markdown文件"
  4. 输入自定义文件名
  5. 等待导出完成
  6. 点击"打开文件"查看结果

🔍 插件特性详解

智能文件过滤

插件会自动忽略以下文件和文件夹:

  • node_modules.gitdistbuild 等构建目录
  • 图片、视频、音频等二进制文件
  • 环境配置文件(.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:文件读取失败

检查文件权限,某些系统文件可能无法读取。

🎯 扩展功能建议

你可以进一步扩展插件功能:

  1. 配置选项:添加用户自定义的忽略规则
  2. 模板支持:支持不同的导出模板
  3. 压缩功能:支持导出为ZIP文件
  4. 云端同步:集成云存储服务
  5. AI集成:直接发送到AI助手进行分析

📝 总结

通过本教程,我们成功开发了一个功能完整的VSCode插件,实现了:

  • ✅ 右键菜单集成
  • ✅ 智能文件过滤
  • ✅ 多语言语法高亮
  • ✅ 详细统计信息
  • ✅ 用户友好的界面
  • ✅ 完整的错误处理

最重要的是,我们解决了常见的打包问题。记住关键点:

  1. 本地安装vscenpm install -D @vscode/vsce
  2. 使用–no-dependencies参数vsce package --no-dependencies
  3. 通过npm脚本执行npm run package

现在你拥有了一个专业的VSCode插件,可以轻松地将任何项目的代码导出为结构化的Markdown文档,非常适合代码分享、文档生成或AI分析使用!


🔗 相关资源:

  • VSCode插件API文档
  • vsce打包工具文档
  • 插件市场发布指南

希望这个教程对你有帮助!如果在开发过程中遇到问题,欢迎在评论区讨论。

http://www.lryc.cn/news/604776.html

相关文章:

  • Python 程序设计讲义(37):字符串的处理方法——设置字符串居中显示:center() 方法
  • 图像平滑处理
  • 9.项目起步(3)
  • OpenCV学习day1
  • 实习小记(个人中心的编辑模块)
  • 商标注册后可以随意更改字体和颜色吗!
  • 怎么理解锁相环主时钟(PLL)怎么做到对时钟进行倍频?
  • Keil STM32工程各文件作用
  • AI框架工具FastRTC快速上手2——整体框架及Stream类详解
  • 方块世界:失落文明的遗产
  • Deforum Stable Diffusion,轻松实现AI视频生成自由!
  • 语音识别dolphin 学习笔记
  • UE5多人MOBA+GAS 番外篇:将冷却缩减属性应用到技能冷却中
  • 设计模式十四:适配器模式(Adapter Pattern)
  • Linux ps -ef 命令解析
  • 基于成像空间转录组技术的肿瘤亚克隆CNV原位推断方法
  • composer 常用命令
  • 智慧城市SaaS平台|市政公用管理系统
  • 从单机到分布式:Redis如何成为架构升级的胜负手
  • 串口接收数据包(协议带帧头帧尾)的编程实现方法:1、数据包格式定义结构体2、使用队列进行数据接收、校验解包
  • 三十二、【Linux网站服务器】搭建httpd服务器演示虚拟主机配置、网页重定向功能
  • uni-app webview 的message无法触发的解决方案
  • MacTex+Vscode数学建模排版
  • 字节跳动“扣子”(Coze)开源:AI智能体生态的技术革命
  • AI Compass前沿速览:可灵创意工坊、字节Coze StudioCoze Loop、通义万相2.2 、智谱GLM-4.5、腾讯混元3D世界模型开源
  • 添加捕捉吸附标识(使用QT+OpenGL开发三维CAD)
  • 【翻译】Label Studio——开源标注工具README.md
  • 2025年DDoS攻防战:六层防护体系构建业务“数字免疫”
  • ADA4622-2ARMZ-R7 ADI双通道精密运算放大器 ±0.25μV超低失调+0.1μV/°C温漂
  • K8s 备份与恢复利器:Velero 实战指南