Skip to content

VS Code 图片注释悬停预览实现说明

复制本地路径 | 在线编辑

简介

插件:vscode-image-comment

这个插件可以在不同语言的代码注释中插入如 markdown 样式的图片格式,鼠标悬停后显示图片预览。

# 在 python 中,鼠标悬停后会显示图片预览
# ![image-comment](.image-comment/image-abc.png)

本文说明这个插件如何在代码注释中识别图片路径,并在鼠标悬停时显示图片预览。

整体流程

插件的工作可以分为两步。本文只聚焦第二步:注释生成之后,插件如何扫描注释并实现悬停预览。

  1. 生成注释。可以使用粘贴功能,插件会在当前位置插入规定的格式。
  2. 解析注释。插件会扫描文档,识别出规定的格式,然后显示图片预览。

显示图片主要有四步:

  1. 根据模板识别图片注释。
  2. 从注释中提取图片路径。
  3. 把图片路径解析成 VS Code 可用的 vscode.Uri
  4. 给该注释行添加 Decoration,并在 Decoration 的 hoverMessage 中显示图片。

1. 根据模板生成正则

默认模板是:

![image-comment]({path})

相关代码在 src/utils/preview.ts

export function createPatternFromTemplate(template: string): RegExp | null {
  const pathPlaceholder = '{path}';
  const placeholderIndex = template.indexOf(pathPlaceholder);

  if (placeholderIndex === -1) {
    return null;
  }

  const beforePath = template.substring(0, placeholderIndex);
  const afterPath = template.substring(placeholderIndex + pathPlaceholder.length);

  const escapedBefore = escapeRegex(beforePath);
  const escapedAfter = escapeRegex(afterPath);

  const patternString = `${escapedBefore}([^\\s"'\\]\\)\\n]+?)${escapedAfter}`;

  try {
    return new RegExp(patternString, 'g');
  } catch {
    return null;
  }
}

以默认模板为例:

template = "![image-comment]({path})"

插件会把它拆成三段:

beforePath = "![image-comment]("
pathPlaceholder = "{path}"
afterPath = ")"

接着把 beforePathafterPath 转义成正则安全字符串。因为 []() 在正则中有特殊含义:

escapedBefore = "!\[image-comment\]\("
escapedAfter = "\)"

然后把 {path} 替换成路径捕获规则,意思是:捕获一段图片路径,直到遇到空白、引号等特殊字符。

([^\s"'\]\)\n]+?)

最终得到的正则大致是:

!\[image-comment\]\(([^\s"'\]\)\n]+?)\)

所以当插件扫描文档时,它可以匹配到规定的格式,并捕获出图片路径。

// ![image-comment](.image-comment/image-abc.png)

2. 扫描文档并提取图片路径

相关代码在 src/utils/preview.ts

export function findImageCommentsInDocument(
  document: vscode.TextDocument,
  template: string,
): ImageMatchResult[] {
  const pattern = createPatternFromTemplate(template);
  if (!pattern) {
    return [];
  }

  const results: ImageMatchResult[] = [];
  const lineCount = document.lineCount;

  for (let i = 0; i < lineCount; i++) {
    const line = document.lineAt(i);
    const lineResults = findImageCommentsInLine(line, pattern);
    results.push(...lineResults);
  }

  return results;
}

这一步没啥好说的,就是扫描文档,识别出规定的格式,解析出 path 字段。

3. 把路径解析成 URI

URI 可以理解为 VS Code 内部用来表示资源位置的对象。

注释里的路径只是普通字符串:

.image-comment/image-abc.png

但 VS Code API 更常使用 vscode.Uri 来表示资源:

vscode.Uri.file("D:\\MyCode\\vscode-image-comment\\.image-comment\\image-abc.png")

相关代码在 src/utils/preview.ts

export function resolveImagePath(
  commentPath: string,
  documentUri: vscode.Uri,
): vscode.Uri | null {
  const workspaceFolder = vscode.workspace.getWorkspaceFolder(documentUri);

  if (path.isAbsolute(commentPath)) {
    if (fs.existsSync(commentPath) && isImageFile(commentPath)) {
      return vscode.Uri.file(commentPath);
    }
    return null;
  }

  if (workspaceFolder) {
    const workspacePath = path.join(workspaceFolder.uri.fsPath, commentPath);
    if (fs.existsSync(workspacePath) && isImageFile(workspacePath)) {
      return vscode.Uri.file(workspacePath);
    }
  }

  const documentDir = path.dirname(documentUri.fsPath);
  const relativeToDocPath = path.join(documentDir, commentPath);
  if (fs.existsSync(relativeToDocPath) && isImageFile(relativeToDocPath)) {
    return vscode.Uri.file(relativeToDocPath);
  }

  return null;
}

它会按顺序尝试:

  1. 如果注释里是绝对路径,就直接检查该文件是否存在。
  2. 如果是相对路径,先按 workspace 根目录拼接。
  3. 如果还找不到,再按当前文档所在目录拼接。

后续显示 hover 图片和打开图片预览都依赖这个 URI。

4. 使用 Decoration 添加悬停预览

Decoration 是 VS Code Extension API 的编辑器装饰能力,也就是 VS Code 支持的一种功能。它不会修改文件内容,只是在编辑器显示层增加视觉效果,例如在这个插件中使用了:

  1. 给图片注释行加背景高亮
  2. 在 gutter,也就是行号左侧区域,显示一个小图标
  3. 给这一行绑定 hoverMessage,鼠标悬停时显示图片预览

相关代码在 src/handlers/imageDecoration.ts

private createDecorationType(): vscode.TextEditorDecorationType {
  const config = vscode.workspace.getConfiguration('imageComment');
  const showGutterIcon = config.get<boolean>('showGutterIcon', true);
  const highlightBackground = config.get<boolean>('highlightBackground', true);

  const decorationOptions: vscode.DecorationRenderOptions = {
    overviewRulerLane: vscode.OverviewRulerLane.Right,
    overviewRulerColor: 'rgba(255, 140, 0, 0.6)',
  };

  // 显示 gutter 图标
  if (showGutterIcon) {
    decorationOptions.gutterIconPath = this.getIconPath('icon.svg');
    decorationOptions.gutterIconSize = '14px';
  }

  // 显示背景高亮
  if (highlightBackground) {
    decorationOptions.backgroundColor = 'rgba(255, 140, 0, 0.08)';
    decorationOptions.border = '1px dashed rgba(255, 140, 0, 0.15)';
    decorationOptions.borderRadius = '2px';
  }

  return vscode.window.createTextEditorDecorationType(decorationOptions);
}

这段代码创建的是一种装饰样式。

真正关键的是下面这行代码,里面的 hoverMessage 就是最后悬停显示的内容。代码在 src/handlers/imageDecoration.tsupdateDecorations 方法中:

decorations.push({
  range: decorationRange,
  hoverMessage: this.createEnhancedHoverMessage(imageUri, match.imagePath),
});

悬停内容由 createEnhancedHoverMessage 生成。它返回的是一个 vscode.MarkdownString,其中包含 Markdown 图片语法,因此 VS Code 的 hover 弹窗可以渲染出图片。

private createEnhancedHoverMessage(imageUri: vscode.Uri, imagePath: string): vscode.MarkdownString {
  const markdown = new vscode.MarkdownString();
  markdown.isTrusted = true;

  const encodedUri = imageUri.toString(true);
  const fsPath = imageUri.fsPath;

  let fileSize = '';

  try {
    const stats = fs.statSync(fsPath);
    fileSize = this.formatFileSize(stats.size);
  } catch {}

  markdown.appendMarkdown(`**📷 ${messages.imageCommentHoverTitle()}**\n\n`);
  markdown.appendMarkdown(`**${messages.imagePathLabel()}:** \`${imagePath}\`\n\n`);

  if (fileSize) {
    markdown.appendMarkdown(`**${messages.imageSizeLabel()}:** ${fileSize}\n\n`);
  }

  // 显示图片预览
  markdown.appendMarkdown(`---\n\n`);
  markdown.appendMarkdown(`![Preview](${encodedUri}|width=300)\n\n`);
  markdown.appendMarkdown(`---\n\n`);
  markdown.appendMarkdown(
    `💡 *${messages.hoverPreviewHint(messages.previewImageLabel())}*`,
  );

  return markdown;
}

Comments

本文阅读 Loading 本站访问 Loading 访客 Loading