ARTICLE · 1110899
我做了个 VS Code 插件:在 .tex 里粘贴图片,像 Markdown 一样省事
我做了个 VS Code 插件:在 .tex 里粘贴图片,像 Markdown 一样省事
作者:water不是水 | 借助 DeepSeek 完成
源码与安装包见文末。
●●●
一、先说说它解决什么
写 LaTeX 的人应该都经历过这个循环:
.tex 所在的文件夹\begin{figure}\centering\includegraphics\caption\label而在 Markdown 里,你只需要 Ctrl+V。
于是我做了这么个插件,让 .tex 也能这样:
截图 →
Ctrl+V→ 图片自动存进.tex同目录 → 自动插入\includegraphics代码 → 光标停在文件名上。
实测演示(这是粘贴后编辑器里的样子):
\begin{figure}[htbp] \centering \includegraphics[width=0.8\linewidth]{PixPin_2026-10-01_15-25-38.jpg} \caption{PixPin_2026-10-01_15-25-38.jpg} \label{fig:PixPin_2026-10-01_15-25-38.jpg}\end{figure}注意图片文件名那里是被选中的。你可以直接开始打字改名,按 Tab 会依次跳到 caption 和 label。文件名与磁盘上的文件是对应的,看一眼就知道有没有写错。
●●●
二、安装(3 步)
前提
必须先装 LaTeX Workshop。
原因是:这个插件通过 VS Code 的「文档类型 = latex」来生效,而这个"类型"是 LaTeX Workshop 提供的语言定义。不装它,VS Code 不会把 .tex 当成 latex 文件,插件就不会触发。
要求 VS Code 1.90 或更高。
安装插件
方法一:图形界面(推荐新手)
tex-paste.vsixCtrl+Shift+X 打开扩展面板...(Views and More Actions)从 VSIX 安装...tex-paste.vsix,装好后按提示重新加载窗口方法二:命令行
code --install-extension tex-paste.vsix装完重载一次窗口(Ctrl+Shift+P → 开发人员: 重新加载窗口)即可生效。
●●●
三、怎么用
基本用法
Ctrl+V完事。图片落在 .tex 同目录,代码插好了,光标套在文件名上。
⚠️ 有一个坑,务必知道
有些截图工具(实测 PixPin)会同时往剪贴板里放两样东西:「图片」和「文件路径」。
这时候 VS Code 认为有多种粘贴方式,不会直接插入,而是弹出一个选择菜单:
插入图片 PixPin_2026-10-01_15-25-38.jpg ← 选这项粘贴为文本...选中带图片图标的那一项就行。
如果没选中(或者按了 Esc),会出现这个"诡异"现象:
为什么要特别强调这个——因为我自己就被它坑过好几次,一度以为插件坏了,跑去查代码。后来才发现是菜单没选中。
所以插件做了兜底:这种情况会弹提示,点一下按钮就能补上,不用重新截图:
TeX Paste:xxx.jpg 已保存到文件夹,但没插入文档 [ 插入最近那张 ]也可以随时手动操作:命令面板 Ctrl+Shift+P 搜
TeX Paste: 插入文件夹里还没被引用的图片它会列出文件夹里所有还没被文档引用的图片,让你挑一张插入。
补充:系统截图(
Win+Shift+S)、从浏览器复制图片、微信里复制图片,这些只有图片没有文件路径,不会有菜单,直接Ctrl+V就行。
支持拖拽
从资源管理器把图片拖进编辑器,效果和粘贴一样(复制到同目录 + 生成代码)。
●●●
四、它能识别哪些图片
Win+Shift+S) | |
| 从 Word / PowerPoint 直接复制图片 |
关于最后一条,得说清楚原因:Word 和 PowerPoint 往剪贴板里放的是 EMF/WMF 格式(矢量图),VS Code 底层是 Electron,不转换这种格式,所以插件拿不到图片数据。
绕过办法:把图片在 Word 里右键「另存为图片」,或者直接拖进编辑器。拖拽那条路是通的。
●●●
五、可调的设置
在设置里搜 texPaste:
texPaste.enabled | true | |
texPaste.figureEnvironment | true | figure 环境;关掉则只插入一行 \includegraphics |
texPaste.width | 0.8\linewidth | width |
texPaste.includeExtension | true | .jpg/.png |
texPaste.overwriteBehavior | nameIncrementally | -2、-3;可改成 overwrite 直接覆盖 |
关于 includeExtension 多说两句。
默认带扩展名({图片.jpg}),这样你一年后回头看,一眼知道文档指向哪个文件。
关掉它则遵循 LaTeX 惯例:\includegraphics{图片} 不带后缀,由 graphicx 自动按 .pdf,.ai,.png,.jpg,.jpeg... 的顺序去找同名文件。两种都能编译,我实测过(顺手验证了 LaTeX 的默认查找顺序)。
两种风格取舍:不带后缀的好处是以后把 .jpg 换成 .pdf(矢量图)时不用改源码。带后缀的好处是清晰。看你习惯。
命令与快捷键
命令面板 Ctrl+Shift+P 搜 TeX Paste,有两个命令:
TeX Paste: 从剪贴板插入图片 | Ctrl+Alt+V |
TeX Paste: 插入文件夹里还没被引用的图片 |
两个命令也在 .tex 编辑器的右键菜单里。
Ctrl+Alt+V 和 Ctrl+V 等价,方便你绑到别的键上。
●●●
六、它不做什么(有意保持简单)
这一条我觉得值得单独说,因为早期版本做了,然后被我删掉了。
早先的版本还支持一个功能:你在文档里改图片文件名,磁盘上的图片文件也跟着改名。
听起来很美好——"文档和磁盘永远同步"。但真机用下来问题很多:
我在实现和调试它的过程中碰到了好几个棘手的时序问题(有一段日志里同一个文件被连续改名为 WENXIAN → WENXIA → WENXIAWENXIAN……)。最后结论是:这个功能的复杂度远超它带来的便利。
所以现在的版本是:
\includegraphics 引用 | |
| 不删除任何文件 |
现在整个插件的文件操作只有一处:写入一张新图片。
// 全项目搜索"会修改用户文件"的调用,只有这一行await vscode.workspace.fs.writeFile(targetUri, data);没有删除、没有改名、没有任何定时器。用起来不会有意外的惊喜。
●●●
七、代码结构(给想改的人)
纯 JavaScript(CommonJS),零构建、零运行时依赖,改完直接生效。
vscode-tex-paste/├── extension.js 476 行 入口:注册 provider / 命令 / 状态栏├── lib/│ ├── clipboard.js 329 行 剪贴板/拖拽 → 字节│ ├── latex.js 299 行 纯函数:文件名清洗、命名、LaTeX 片段│ └── snippet.js 160 行 纯函数:snippet 模板、定位 \includegraphics├── package.json 扩展清单:设置项 / 命令 / 快捷键 / 菜单├── test/ 测试(下面细说)├── scripts/install.ps1 打包 + 安装脚本└── README.md生产代码约 1260 行,其中相当一部分是注释(尤其是"为什么这么写"的说明)。
各模块职责
extension.js — 入口
注册两个 provider 和两个命令,装配其他模块:
vscode.languages.registerDocumentPasteEditProvider(SELECTOR, newLatexPasteProvider(), {providedPasteEditKinds: [PASTE_KIND],pasteMimeTypes: MIME_TYPES,});关键点:MIME_TYPES 必须在这里声明清楚,否则 provider 根本收不到对应类型的数据。声明的是:
constMIME_TYPES = ['text/uri-list', 'files', 'image/*', 'text/html'];text/uri-list —— 资源管理器里复制/拖拽文件files —— 系统剪贴板里的文件image/* —— 截图、浏览器图片text/html —— 有些来源只给内嵌的 base64 data: URLlib/latex.js — 命名与清洗(纯函数,无 VS Code 依赖)
负责:MIME ↔ 扩展名映射、image-2026-10-01-151314 这类时间戳命名、文件名校验。
校验这一块比较讲究,比如拒绝花括号:
// 允许 "a}b" 会插入出 \includegraphics{a}b} 这种畸形命令if (LATEX_HOSTILE_CHARS.test(s)) {return { ok: false, reason: '文件名不能包含花括号 { }(会破坏 LaTeX 命令结构)' };}还拒绝 Windows 保留设备名(CON、PRN、COM1……)、首尾点和下划线、非法字符、超长名字。
lib/snippet.js — 生成插入代码
生成两个版本,必须严格同构:
plain —— 字面文本,文件名原样出现(用来在文档里精确搜索,确认插入是否真的发生)snippet —— 带 ${1:...} tabstop 的版本(这才决定光标停在哪)const escName = latex.escapeSnippet(displayName(o));` \\includegraphics${widthOpt}{\${1:${escName}}}`${1:...} 就是那个"光标套在文件名上"的机制。${2:...} 给 caption,${3:...} 给 label。
lib/clipboard.js — 从剪贴板拿字节
多来源回退:image/* → dataTransferFile → text/uri-list → HTML 内嵌 data URL。
而且不信 MIME 声明,用魔数校验:
functionimageSignatureKind(bytes) {if (at(0) === 0x89 && at(1) === 0x50 && at(2) === 0x4e && at(3) === 0x47) return'png';if (at(0) === 0xff && at(1) === 0xd8 && at(2) === 0xff) return'jpeg';// ...}命名逻辑
PixPin_2026-10-01_15-25-38.jpg)image-2026-10-01-151314-2、-3……(除非你改成 overwrite)●●●
八、测试(这一节是重点)
做这个"小"插件的过程中,我最大的收获是:真机测试和单元测试完全是两回事。
这个项目有 4 层测试,共 78 个用例:
| 真 xelatex 编译 | ||
| 真 VS Code 集成测试 |
最有价值的那一层
集成测试会启动一个真实的 VS Code 实例,加载插件,然后:
还有这条我特意加的回归用例,它对应的是一个真实踩过的坑:
✔ 回归:弹出粘贴菜单磨蹭 8 秒后仍能插入(确认不再有时限)因为早期版本有个 5 秒"确认时限",从弹出菜单到你点中「插入图片」经常超过 5 秒,记录先被清掉,导致后续操作错乱。这种问题单元测试永远测不出来——它只在真实交互的时序里出现。
编译验证那一层
不是"看起来对",而是真跑一次 xelatex:
.texxelatex -interaction=nonstopmode -halt-on-errornot found顺带也验证了中文文件名(ctexart + xelatex)能不能编过。
●●●
九、开发过程中踩的坑(值得记录)
我把这些写下来,因为它们都是真机测试才能发现的:
1. 一次粘贴,provider 被调用 4 次
真机日志显示,一次 Ctrl+V 会触发 4 次 provideDocumentPasteEdits。
如果每次都落盘,一次粘贴就会生成 4 个不同时间戳的文件。
解决:用剪贴板数据的指纹做短时缓存。
2. VS Code 会随编辑平移你记录的那个 Range
这个坑最隐蔽。我本来记录了"文件名所在的区间",想用它判断"光标是不是还在文件名里"。
但你在花括号里打字时,文本长度变了,VS Code 会把那个 Range 一起平移。于是我的锚点从 \includegraphics 那一行"漂"到了 \caption 里,读到的是错误的旧名字,改名被静默跳过。
教训:记录行号 + 列号,每次现算。不要依赖 Range 的偏移。
3. new vscode.Range(a, b) 不接受偏移量
我写 new vscode.Range(17, 24) 想表示"第 17 到第 24 个字符",结果抛出 Invalid arguments。
Range 的构造函数只接受 Position 对象。偏移量形式是静态方法Range.create 或者自己用 document.positionAt() 换算。
4. 文件名出现在 figure 模板里三次
\includegraphics{NAME}、\caption{NAME}、\label{fig:NAME}。
想精确定位"第一处",不能简单 indexOf(name)——得先找到 \includegraphics,再取它后面第一个 {} 里的内容。
5. LaTeX 不带扩展名也能找到图
我一开始不确定 \includegraphics{图片} 不带后缀行不行,所以专门写了个脚本验证。
结论:行。xelatex 自己会打印出默认的查找顺序:
.pdf,.PDF,.ai,.AI,.png,.PNG,.jpg,.JPG,.jpeg,.JPEG,.jp2graphicx 会按这个顺序依次拼后缀去找文件。
●●●
十、关于我和 DeepSeek 的分工
这个插件是我提需求、定方向、做真实场景验证;代码实现和调试验证是 DeepSeek 写的。
我的实际感受是:AI 写代码很快,但"哪些地方真会踩坑"必须靠真实环境暴露。
举几个例子:
Ctrl+Shift+B(运行生成任务)当成了保存;以为图片被删了,其实是改名成了别的名字。所以我的用法是:
AI 负责写代码和写测试;我负责把它放到真实场景里跑,然后把日志和现象反馈回去。
这个循环转了几十轮,才有了现在这版"看起来很简单"的插件。简单是删出来的,不是一开始就简单。
早期版本有改名联动、有引用自动修复、有各种安全策略,最后都被我删掉了——因为它们带来的复杂度超过了价值。
●●●
十一、下载
通过网盘分享的文件:tex-paste.vsix
链接: https://pan.baidu.com/s/1BzIqNYeupxBd8gpG--RpZw?pwd=faun提取码: faun
(来自百度网盘超级会员 v6 的分享)
安装回顾
Ctrl+Shift+X → 右上角 ... → 从 VSIX 安装... → 选 tex-paste.vsixCtrl+Shift+P → 开发人员: 重新加载窗口)一句话用法
在 .tex 里 Ctrl+V。若弹出选择菜单,选「插入图片 xxx.jpg」那一项。
●●●
附:技术信息一览
local.tex-paste | |
| 零 | |
registerDocumentPasteEditProviderregisterDocumentDropEditProvider、workspace.fs.writeFile | |
有任何问题或者想加功能,欢迎留言告诉我。
●●●
本文由 water不是水 撰写,代码实现借助 DeepSeek 完成。