开发一个 vscode 图片悬停预览插件:用 MarkdownString 实现 hover 图片预览
1. 从「点开才知道是什么图」说起VS Code 悬停图片预览插件到底解决什么问题写前端项目的时候你一定遇到过这种场景代码里散落着一堆图片地址有的是本地assets目录下的相对路径有的是 CDN 上的绝对 URL还有的是接口返回的临时链接。想确认某一行到底引用的是哪张图只能复制链接、切到浏览器、粘贴、回车看完再切回来。一个页面里十几张图来回切十几次思路全断了。这就是 VS Code 悬停图片预览插件要解决的核心痛点让鼠标停在图片链接上时直接弹出缩略图不用离开编辑器。它属于 VS Code 插件开发里非常典型的一类需求——利用HoverProvider拦截鼠标悬停事件再用MarkdownString把图片渲染进悬停浮层。听起来像黑魔法实际上核心代码不到 30 行。这篇文章面向三类人一是想入门 VS Code 插件开发、但被官方文档劝退的前端同学二是已经用过 Image Preview 之类插件、却发现悬停不生效想自己造轮子的开发者三是想搞懂HoverProviderMarkdownString这套组合拳到底怎么配合的人。我会从零带你走完初始化项目、配置package.json激活事件、注册 HoverProvider、正则提取当前行图片链接、用MarkdownString渲染图片、限制宽高、最后在 Extension Host 里实测验证。需要说明的是本文聚焦的是插件本身的实现路径不涉及任何网络访问工具。图片链接能不能加载取决于你本机网络环境能否直接访问该地址这一点在调试时要注意。另外如果你在插件里需要调用大模型来做一些智能补全或图片描述生成可以了解下 TaoToken 这类模型 API 聚合服务它提供统一的接口入口后面我会在接入部分简单提一下配置方式但主线仍然是插件开发本身。先明确一个概念VS Code 插件本质上是一个 Node.js 模块通过activate函数在特定时机被激活然后向 VS Code 注册各种「提供者」Provider。HoverProvider就是其中一种你告诉 VS Code「当用户在某种语言的文件里悬停时调用我的函数」VS Code 就把当前文档对象和鼠标位置传给你你返回一个Hover对象它就负责渲染。MarkdownString则是Hover支持的内容类型之一它允许你用 Markdown 语法写内容而 Markdown 的图片语法正好能被 VS Code 渲染成真实图片。整条链路就是这么直白。2. 动手前的准备TaoToken 模型接入与插件工程初始化这一节分两部分一是插件工程怎么搭起来二是如果你想让插件具备「调用模型生成图片描述」这类扩展能力怎么把 TaoToken 的接口配进去。先做主线。2.1 创建插件项目骨架打开终端执行三条命令mkdir image-preview cd image-preview npm init -y touch index.jsnpm init -y会生成一个默认的package.json。但默认内容不够用VS Code 插件必须声明engines.vscode和activationEvents两个字段否则插件根本不会被加载。把package.json改成下面这样{ name: image-preview, version: 1.0.0, description: hover 图片预览插件, main: index.js, scripts: { test: echo \Error: no test specified\ exit 1 }, keywords: [vscode, hover, image-preview], author: , engines: { vscode: ^1.54.0 }, activationEvents: [*], license: ISC }这里有两个关键点。engines.vscode声明了插件兼容的最低 VS Code 版本^1.54.0表示 1.54.0 及以上都行。activationEvents里的*表示 VS Code 一启动就激活插件——这是最省事的写法适合开发调试阶段。生产环境更推荐用onLanguage:javascript这种按需激活减少启动开销。2.2 如果你要接入 TaoToken 做扩展能力假设你想给插件加一个功能悬停图片时顺便调用模型生成一句图片内容描述。这时候就需要一个模型 API。TaoToken 提供统一的 API 入口Base URL 是https://taotoken.net/api你需要在控制台创建一个 API Key然后在插件里用fetch或axios调用。配置方式很简单在插件项目根目录建一个.env或者直接在代码里读 VS Code 的配置项。推荐用 VS Code 的workspace.getConfiguration读取用户设置避免把 Key 硬编码进代码。一个典型的请求体长这样{ model: claude-sonnet-4-20250514, max_tokens: 256, messages: [ { role: user, content: 用一句话描述这张图片的内容https://example.com/test.png } ] }请求头发Authorization: Bearer 你的Key请求地址https://taotoken.net/api/v1/messages。注意这一步是可选扩展本文主线不依赖它。如果你只是想实现悬停预览完全不需要任何 API Key纯本地正则 MarkdownString 就够了。想拿 Key 的话去控制台的 API Keys 页面创建即可https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 里面有各语言的调用示例。2.3 安装调试依赖VS Code 插件开发不需要额外安装运行时依赖vscode模块是 VS Code 内置提供的不要npm install vscode那样会报错。你只需要在项目里放一个.vscode/launch.json来配置调试{ version: 0.2.0, configurations: [ { name: Run Extension, type: extensionHost, request: launch, args: [--extensionDevelopmentPath${workspaceFolder}] } ] }配好之后按 F5VS Code 会打开一个新的「扩展开发宿主」窗口你的插件就在那个窗口里生效。这个窗口和普通 VS Code 窗口没区别可以打开任意项目测试。3. 可复制配置HoverProvider 注册与 MarkdownString 图片渲染完整代码这一节是全文核心直接给可运行的完整代码然后逐段拆解。3.1 最小可运行版本先写一个「悬停显示 hello」的版本确认 HoverProvider 注册成功const vscode require(vscode); module.exports.activate function (context) { context.subscriptions.push( vscode.languages.registerHoverProvider(javascript, { provideHover: (document, position) { return new vscode.Hover(hello); }, }) ); };registerHoverProvider第一个参数是语言 IDjavascript表示只对 JS 文件生效。第二个参数是提供者对象必须实现provideHover方法。context.subscriptions.push的作用是把注册的 provider 挂到插件生命周期上插件卸载时自动清理避免内存泄漏。3.2 提取当前行图片链接接下来要拿到鼠标悬停那一行的文本用正则匹配出 URLconst vscode require(vscode); module.exports.activate function (context) { context.subscriptions.push( vscode.languages.registerHoverProvider(javascript, { provideHover: (document, position) { const { _line } position; const lineContent document.lineAt(_line).text; const regexp /((https?):)?\/\/[-A-Za-z0-9#/%?~_|!:,.;][-A-Za-z0-9#/%~_|]/; const res lineContent.match(regexp); if (res null) { return; } const url res[0]; return new vscode.Hover(hello); }, }) ); };position._line是当前悬停的行号document.lineAt(_line).text取出整行文本。正则匹配http://或https://开头的链接。如果没匹配到就返回undefinedVS Code 就不显示悬停浮层。3.3 用 MarkdownString 渲染图片关键一步来了。Hover的构造函数除了接受字符串还接受MarkdownString。而 Markdown 的图片语法会被 VS Code 渲染成真实图片const vscode require(vscode); module.exports.activate function (context) { context.subscriptions.push( vscode.languages.registerHoverProvider(javascript, { provideHover: (document, position) { const { _line } position; const lineContent document.lineAt(_line).text; const regexp /((https?):)?\/\/[-A-Za-z0-9#/%?~_|!:,.;][-A-Za-z0-9#/%~_|]/; const res lineContent.match(regexp); if (res null) { return; } const url res[0]; return new vscode.Hover(new vscode.MarkdownString()); }, }) ); };注意MarkdownString默认是「受限模式」某些 Markdown 特性比如 HTML 标签不会渲染。图片语法是支持的所以直接写就行。3.4 限制图片宽高图片太大撑爆浮层怎么办在 URL 后面加|width240或|height180return new vscode.Hover(new vscode.MarkdownString());这个语法是 VS Code 特有的扩展不是标准 Markdown。width和height可以只写一个另一个按比例缩放。3.5 完整配置对照表配置项作用推荐值engines.vscode最低兼容版本^1.54.0activationEvents激活时机调试用*生产用onLanguage:javascriptregisterHoverProvider语言 ID生效范围javascript/typescript/*MarkdownString图片语法渲染图片宽高限制控制浮层大小|width240如果你后续要接入 TaoToken 做图片描述生成可以在provideHover里加一个异步请求把模型返回的文本拼到 MarkdownString 里。但要注意provideHover支持返回Thenable也就是可以 async。配置 Base URL 用https://taotoken.net/apiKey 从配置读Model ID 按你选的模型填。4. 在 Extension Host 里验证悬停请求与成功结果实测代码写完了怎么确认它真的生效这一节讲完整的验证流程。4.1 启动调试在项目窗口按 F5VS Code 会弹出一个新的「扩展开发宿主」窗口。这个窗口标题栏会显示[Extension Development Host]底部状态栏是橘色的表示当前处于调试模式。如果你没看到橘色状态栏说明调试没启动成功检查.vscode/launch.json是否配置正确。4.2 准备测试文件在调试窗口里新建一个.js文件写入一行测试内容const url https://ai-sample.oss-cn-hangzhou.aliyuncs.com/test/695fd240c6c011eb99f4db4397160818.png;把鼠标移到这个 URL 上停留一秒左右应该会弹出一个浮层里面显示图片缩略图。如果显示的是hello说明你还在用旧代码点调试工具栏的绿色刷新按钮重新加载。4.3 查看 DEBUG CONSOLE在调试窗口里打开View Terminal切到DEBUG CONSOLE面板。当你在 URL 上悬停时控制台会打印出正则匹配的结果。如果看到类似[https://...png, https:, ...]的输出说明正则工作正常。如果什么都没打印检查provideHover是否被调用——可能是语言 ID 不匹配比如你在.ts文件里测试但注册的是javascript。4.4 验证宽高限制把代码改成点绿色刷新按钮再次悬停。图片应该被限制在 240px 宽。如果没变化可能是 VS Code 版本太老不支持这个语法升级到 1.54 以上即可。4.5 实测结果说明正常情况下从悬停到图片显示大约有 200-500ms 延迟取决于图片大小和网络速度。如果图片地址无法访问比如内网地址、需要鉴权的 CDN浮层会显示一个破图图标。这不是插件的问题是图片本身加载失败。你可以在MarkdownString里加一段文字说明比如const md new vscode.MarkdownString(\n\n[打开原图](${url}));这样即使图片加载失败用户也能点击链接去浏览器打开。5. 常见报错排查401、local proxy failed、reading choices 与 OAuth 问题这一节整理我在开发和接入过程中踩过的坑以及对应的排查思路。5.1 插件完全不生效悬停没反应最常见的原因是activationEvents没配或配错。如果你写的是onLanguage:javascript但测试文件是.jsx语言 ID 其实是javascriptreact就不会触发。调试阶段建议先用*确认逻辑没问题再收窄。另一个原因是main字段指向的入口文件路径不对。package.json里的main必须是相对于项目根目录的路径比如./index.js或index.js。5.2 报错401 Unauthorized这个报错通常出现在你接入了模型 API 的场景。原因一般是 API Key 没传、传错、或者过期。检查请求头Authorization: Bearer Key是否正确Key 有没有多余空格。如果你用的是 TaoToken去控制台确认 Key 状态是否正常https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。5.3 报错local proxy failed或连接超时这个报错说明请求根本没发出去或者被本机网络环境拦截了。先确认你的网络能直接访问目标地址用curl测一下curl -I https://taotoken.net/api/v1/messages如果curl也失败那就是网络问题和插件代码无关。如果curl成功但插件失败检查插件里用的 HTTP 库是否走了系统代理设置。5.4 报错reading choices或返回结构解析失败这类报错通常是因为你按 OpenAI 的返回格式去解析但实际接口返回的是 Anthropic 格式。OpenAI 的响应里是choices[0].message.contentAnthropic 的是content[0].text。接入前先看文档确认返回结构https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。如果你用的是 Claude Code 这类工具配置方式又不一样需要设置ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY两个环境变量。5.5 OAuth 相关报错如果你在插件里集成了需要 OAuth 登录的服务可能会遇到OAuth callback failed或invalid redirect_uri。这类问题的根源是回调地址没在服务商后台登记。VS Code 插件做 OAuth 比较麻烦因为vscode://协议的回调需要额外注册。建议先用 API Key 方式简单直接。5.6 三件套配置检查清单无论你接入哪个模型服务都要确认三件套齐全配置项示例值说明Base URLhttps://taotoken.net/api接口根地址不要带末尾斜杠API Keysk-xxxx从控制台创建Model IDclaude-sonnet-4-20250514按文档填别自己编如果你用的是 Cline、CC Switch 这类工具配置界面里也是填这三项。Codex 的话是写进auth.json格式略有不同但本质一样。6. 从能用到好用继续打磨你的悬停预览插件代码跑通只是起点。一个真正顺手的插件还需要处理这些细节。过滤非图片链接。当前正则会把所有 URL 都当图片渲染包括 API 地址。可以在渲染前判断扩展名const imageExts [.png, .jpg, .jpeg, .gif, .webp, .svg]; const isImage imageExts.some(ext url.toLowerCase().includes(ext)); if (!isImage) { return; }支持更多语言。把registerHoverProvider的第一个参数改成*可以覆盖所有语言但副作用是 DEBUG CONSOLE 里的内容也会触发。更稳妥的做法是注册多个语言 ID比如[javascript, typescript, javascriptreact, typescriptreact, vue, html]。支持 http 协议。有些内网图片是 http 的正则里已经包含了https?但 VS Code 的 MarkdownString 默认可能拦截 http 图片。如果遇到不显示的情况检查 VS Code 设置里的markdown.preview相关项。允许用户配置宽高。在package.json里加contributes.configuration暴露一个imagePreview.width配置项然后在代码里读const config vscode.workspace.getConfiguration(imagePreview); const width config.get(width, 240);打包发布。安装vsce后执行vsce package生成.vsix文件别人可以直接安装。发布到市场需要注册 publisher 账号配好后vsce publish一行命令搞定。如果你想让插件具备更智能的能力比如自动识别图片内容并生成 alt 文本可以接入模型 API。TaoToken 的 Coding Plan 适合长期做这类 Agent 开发https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。想先试试模型对话效果的话这里可以直接体验https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。最后留一个实用技巧调试插件时如果改了代码但悬停效果没变不一定要重启整个调试窗口。在 DEBUG CONSOLE 里执行Developer: Reload Window命令或者直接点调试工具栏的绿色刷新按钮通常就能加载新代码。但如果改的是package.json里的activationEvents或contributes那就必须完全重启调试会话因为这些配置只在插件加载时读取一次。