EditorDecoration 编辑器装饰
装饰(Decoration)是在不修改文档内容的前提下,给代码「上色打标」的能力:关键词高亮、错误波浪线、行号着色、边距图标都属于装饰。它是代码可读性与即时反馈的核心手段。
装饰的两层结构
装饰由「类型」与「范围」组合而成:
TextEditorDecorationType(样式:颜色、字体、图标)
+
DecorationOptions(位置:哪个范围、带什么提示)
↓
textEditor.setDecorations(type, options)- 类型:描述装饰长什么样,创建一次可反复使用
- 范围:描述装饰画在哪,随文档变化动态更新
- 渲染:
setDecorations把两者结合应用到编辑器
创建装饰类型
window.createTextEditorDecorationType 创建样式类型:
typescript
import * as vscode from 'vscode';
// 关键词高亮:加粗 + 蓝色
const keywordDecoration = vscode.window.createTextEditorDecorationType({
color: '#569cd6',
fontWeight: 'bold'
});
// 错误波浪线:红色下波浪
const errorDecoration = vscode.window.createTextEditorDecorationType({
textDecoration: 'underline wavy red',
overviewRulerColor: 'red'
});
// 行背景高亮
const highlightLine = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(255, 255, 0, 0.2)',
isWholeLine: true
});常见样式字段
| 字段 | 作用 | 示例值 |
|---|---|---|
color | 前景色 | '#569cd6' |
backgroundColor | 背景色 | 'rgba(0,0,255,0.1)' |
fontWeight | 字重 | 'bold'、'normal' |
fontStyle | 字体样式 | 'italic' |
textDecoration | 文本装饰 | 'underline wavy red' |
border | 边框 | '1px solid red' |
isWholeLine | 是否整行装饰 | true |
overviewRulerColor | 滚动条预览条颜色 | 'red' |
overviewRulerLane | 预览条位置 | vscode.OverviewRulerLane.Left |
gutterIconPath | 行号区图标 | Uri 或 {dark, light} |
before / after | 前后插入内容 | { contentText, color } |
rangeBehavior | 编辑时范围行为 | vscode.DecorationRangeBehavior.ClosedClosed |
应用与更新装饰
TextEditor.setDecorations(type, options) 应用装饰,重复调用即覆盖更新:
typescript
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
const ranges: vscode.Range[] = [];
for (let line = 0; line < editor.document.lineCount; line++) {
const text = editor.document.lineAt(line).text;
// 找出所有「TODO」
const regex = /TODO/g;
let match: RegExpExecArray | null;
while ((match = regex.exec(text))) {
const start = new vscode.Position(line, match.index);
const end = new vscode.Position(line, match.index + match[0].length);
ranges.push(new vscode.Range(start, end));
}
}
// 应用装饰(可传 Range 数组或 DecorationOptions 数组)
editor.setDecorations(keywordDecoration, ranges);DecorationOptions 装饰范围
只传 Range 只能画「色块」。要带悬停提示、图标、前后缀内容,需要 DecorationOptions:
typescript
const options: vscode.DecorationOptions[] = [];
// 悬停提示
options.push({
range: new vscode.Range(0, 0, 0, 4),
hoverMessage: new vscode.MarkdownString('**这里是提示**,支持 Markdown')
});
// 边距图标 + 提示
options.push({
range: new vscode.Range(2, 0, 2, 10),
hoverMessage: '此行有备注',
renderOptions: {
dark: { after: { contentText: '● 备注', color: '#888888' } },
light: { after: { contentText: '● 备注', color: '#666666' } }
}
});
editor.setDecorations(annotationDecoration, options);DecorationOptions 完整字段
| 字段 | 作用 |
|---|---|
range | 装饰覆盖的范围 |
hoverMessage | 悬停显示的内容(字符串或 MarkdownString) |
renderOptions | 针对 dark/light 主题的渲染细节(before/after 等) |
overviewRulerColor | 覆盖类型级颜色,单独标记该范围 |
before/after 插入内容
after 在范围末尾追加内容,常用于行尾标注:
typescript
const lineEndNote = vscode.window.createTextEditorDecorationType({
after: {
contentText: ' ← 未提交',
color: 'orange',
margin: '0 0 0 16px'
}
});
// 对整行应用(isWholeLine 使 after 显示在行尾)
const whole = new vscode.Range(line, 0, line, 0);
editor.setDecorations(lineEndNote, [{
range: whole,
hoverMessage: '此行包含未提交的修改'
}]);动态装饰的性能优化
编辑器输入时文档频繁变化,装饰也要跟着变。全量重算会卡顿,需要优化策略。
防抖合并
监听变更事件时不要立即重算,用防抖把短时间内的多次变更合并为一次:
typescript
export function activate(context: vscode.ExtensionContext) {
const decoration = vscode.window.createTextEditorDecorationType({
color: '#d19a66', fontWeight: 'bold'
});
let timer: ReturnType<typeof setTimeout> | undefined;
context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor((editor) => {
if (editor) {
scheduleUpdate(editor);
}
}),
vscode.workspace.onDidChangeTextDocument((event) => {
const editor = vscode.window.activeTextEditor;
if (editor && event.document === editor.document) {
scheduleUpdate(editor);
}
})
);
function scheduleUpdate(editor: vscode.TextEditor): void {
// 300ms 内多次触发只算一次
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
applyDecorations(editor, decoration);
}, 300);
}
}
function applyDecorations(
editor: vscode.TextEditor,
decoration: vscode.TextEditorDecorationType
): void {
const ranges: vscode.Range[] = [];
const doc = editor.document;
// 正则快速扫描
const regex = /important|critical/gi;
let match: RegExpExecArray | null;
while ((match = regex.exec(doc.getText()))) {
ranges.push(new vscode.Range(
doc.positionAt(match.index),
doc.positionAt(match.index + match[0].length)
));
}
editor.setDecorations(decoration, ranges);
}增量更新:只重算变更行
大文件下整文档扫描开销大,只更新发生变化的行:
typescript
// 模块级装饰类型:增量更新与防抖全量共用
const decoration = vscode.window.createTextEditorDecorationType({
color: '#d19a66',
fontWeight: 'bold'
});
function applyIncremental(
editor: vscode.TextEditor,
decoration: vscode.TextEditorDecorationType,
changedLines: number[]
): void {
const ranges: vscode.Range[] = [];
const doc = editor.document;
// 只扫描变更行
for (const line of changedLines) {
if (line >= doc.lineCount) {
continue;
}
const text = doc.lineAt(line).text;
const regex = /important|critical/gi;
let match: RegExpExecArray | null;
while ((match = regex.exec(text))) {
ranges.push(new vscode.Range(line, match.index, line,
match.index + match[0].length));
}
}
// 增量更新:setDecorations 会与旧结果合并,
// 需要与「旧范围集合」做差集才能精确移除
editor.setDecorations(decoration, ranges);
}
// 变更事件提供变更行号
vscode.workspace.onDidChangeTextDocument((event) => {
const changedLines = Array.from(new Set(
event.contentChanges.map((c) => c.range.start.line)
));
const editor = vscode.window.activeTextEditor;
if (editor && event.document === editor.document) {
applyIncremental(editor, decoration, changedLines);
}
});性能对比
| 策略 | 开销 | 适用场景 |
|---|---|---|
| 全量重算 | 高,O(全文) | 中小文件、低频更新 |
| 防抖合并 | 中 | 高频输入时的通用兜底 |
| 增量更新 | 低,O(变更行) | 大文件、语言服务型插件 |
多个装饰类型分层
一个文档同时叠加多种装饰是常态:语义高亮一层、诊断波浪线一层、书签图标一层。每种职责一个类型,互不干扰:
typescript
// 分层管理:每一层独立创建与更新
const semanticLayer = vscode.window.createTextEditorDecorationType({
color: '#ce9178'
});
const errorLayer = vscode.window.createTextEditorDecorationType({
textDecoration: 'underline wavy red',
overviewRulerColor: 'red',
overviewRulerLane: vscode.OverviewRulerLane.Right
});
const bookmarkLayer = vscode.window.createTextEditorDecorationType({
gutterIconPath: vscode.Uri.joinPath(context.extensionUri,
'media', 'bookmark.svg'),
backgroundColor: 'rgba(255, 200, 0, 0.15)',
isWholeLine: true
});
// 分别更新各自的范围,互不覆盖
editor.setDecorations(semanticLayer, semanticRanges);
editor.setDecorations(errorLayer, errorOptions);
editor.setDecorations(bookmarkLayer, bookmarkRanges);层间覆盖的渲染顺序
| 维度 | 优先级说明 |
|---|---|
| 前景色 | 后创建的装饰类型颜色生效范围以 setDecorations 顺序为准 |
| 文本装饰 | 波浪线与下划线可共存 |
| 整行背景 | 叠加为混合色 |
| 边距图标 | 多图标会同时显示 |
完整示例:关键词高亮 + 错误波浪线
做一个迷你「代码质量检查器」:扫描 FIXME 高亮为警告色(带悬停提示),扫描超长行画红色波浪线,二者分层渲染:
typescript
// extension.ts
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
// 层 1:FIXME 关键词高亮
const fixmeLayer = vscode.window.createTextEditorDecorationType({
color: '#e5c07b',
backgroundColor: 'rgba(229, 192, 123, 0.15)',
fontWeight: 'bold',
overviewRulerColor: '#e5c07b',
overviewRulerLane: vscode.OverviewRulerLane.Center
});
// 层 2:超长行错误波浪线
const longLineLayer = vscode.window.createTextEditorDecorationType({
textDecoration: 'underline wavy red',
overviewRulerColor: 'red'
});
// 层 3:行尾空格(Hint 级别,浅色背景)
const trailingLayer = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(255, 0, 0, 0.1)'
});
// 更新当前编辑器
function updateDecorations(editor: vscode.TextEditor): void {
const doc = editor.document;
const fixmeOptions: vscode.DecorationOptions[] = [];
const longLineOptions: vscode.DecorationOptions[] = [];
const trailingRanges: vscode.Range[] = [];
const MAX_LINE = 100;
for (let line = 0; line < doc.lineCount; line++) {
const text = doc.lineAt(line).text;
// 1) FIXME 关键词(带悬停提示)
const fixmeRegex = /\bFIXME\b/g;
let m: RegExpExecArray | null;
while ((m = fixmeRegex.exec(text))) {
fixmeOptions.push({
range: new vscode.Range(line, m.index, line,
m.index + m[0].length),
hoverMessage: new vscode.MarkdownString(
'**待修复标记**:' + text.slice(m.index)
)
});
}
// 2) 超长行
if (text.length > MAX_LINE) {
longLineOptions.push({
range: new vscode.Range(line, MAX_LINE, line, text.length),
hoverMessage: `此行 ${text.length} 字符,超过 ${MAX_LINE}`
});
}
// 3) 行尾空格
const trailing = text.match(/\s+$/);
if (trailing && trailing.index !== undefined) {
trailingRanges.push(new vscode.Range(
line, trailing.index, line, text.length
));
}
}
// 三个层分别提交
editor.setDecorations(fixmeLayer, fixmeOptions);
editor.setDecorations(longLineLayer, longLineOptions);
editor.setDecorations(trailingLayer, trailingRanges);
}
// 防抖调度
let timer: ReturnType<typeof setTimeout> | undefined;
function schedule(editor: vscode.TextEditor): void {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => updateDecorations(editor), 200);
}
// 事件挂载:编辑器切换 + 文档变更 + 文档打开
context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor((editor) => {
if (editor) {
schedule(editor);
}
}),
vscode.workspace.onDidChangeTextDocument((event) => {
const editor = vscode.window.activeTextEditor;
if (editor && event.document === editor.document) {
schedule(editor);
}
}),
vscode.workspace.onDidOpenTextDocument((doc) => {
const editor = vscode.window.activeTextEditor;
if (editor && editor.document === doc) {
schedule(editor);
}
})
);
// 资源回收
context.subscriptions.push(
fixmeLayer, longLineLayer, trailingLayer
);
}效果:代码里 FIXME 被琥珀色加粗标记,悬停显示原文;超过 100 字符的行末端拖红色波浪线;行尾空格浅红背景。三层互不干扰,各自独立更新。
常见问题
| 问题 | 处理 |
|---|---|
| 装饰不显示 | 确认装饰类型未 dispose、范围有效 |
| 更新不生效 | setDecorations 是覆盖式,旧范围要先移除 |
| 输入时闪烁 | 引入防抖,合并高频更新 |
| 大文件卡顿 | 增量更新,只重算变更行 |
| 图标不显示 | 检查 gutterIconPath 路径与主题模式(dark/light) |
| 悬停提示无 Markdown | 使用 vscode.MarkdownString 并开启 isTrusted |
| 深色/浅色显示异常 | 用 renderOptions 的 dark/light 分支分别配置 |
装饰是「只读视觉层」,不改动文档就能传递信息。分层管理、防抖更新,是把它用好的两个关键习惯。