Editor 选区与游标
编辑器里「光标在哪、选中了什么、屏幕上能看到什么」是三类高频需求:跳转到定义、批量改名、多光标插入。VSCode 的 TextEditor API 围绕 Position、Range、Selection 三个坐标概念构建,掌握它们就能精确控制编辑器的视口与选区。
坐标三件套:Position / Range / Selection
| 类型 | 含义 | 表示 |
|---|---|---|
Position | 一个点(行、列) | new Position(3, 7) |
Range | 一段区间(起止两个点) | new Range(start, end) |
Selection | 带方向的光标选区(anchor + active) | new Selection(anchor, active) |
typescript
import * as vscode from 'vscode';
// Position:行 3、列 7(0 起计数)
const pos = new vscode.Position(3, 7);
// Range:从(3,7)到(5,2)
const range = new vscode.Range(
new vscode.Position(3, 7),
new vscode.Position(5, 2)
);
// 快捷构造:起止坐标直接传
const range2 = new vscode.Range(3, 7, 5, 2);
// Selection:anchor 固定端,active 游标端
// active 在 anchor 之前表示「反向选择」
const sel = new vscode.Selection(3, 7, 5, 2);
console.log(sel.isReversed); // active 在 anchor 前时为 truePosition 常用操作
typescript
const p = new vscode.Position(3, 7);
p.translate(2, -3); // 向下 2 行、左移 3 列 -> (5, 4)
p.with(undefined, 0); // 保留行号、列改为 0 -> (3, 0)
p.line; // 3
p.character; // 7
p.isBefore(new vscode.Position(4, 0)); // trueRange 常用操作
typescript
const r = new vscode.Range(3, 7, 5, 2);
r.start; // Position(3,7)
r.end; // Position(5,2)
r.isEmpty; // 起止相同则 true
r.isSingleLine; // 是否同一行
r.contains(new vscode.Position(4, 0)); // true
r.union(new vscode.Range(0, 0, 1, 1)); // 并集
r.intersection(new vscode.Range(4, 0, 6, 0)); // 交集或 undefinedselection 与 selections:多光标管理
主选区与全部选区
typescript
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
// 单个主选区(光标只有一个时也返回选区)
const selection: vscode.Selection = editor.selection;
// 全部选区(多光标时数量大于 1)
const selections: readonly vscode.Selection[] = editor.selections;
console.log(`当前有 ${selections.length} 个光标/选区`);设置多光标
给 editor.selections 赋值数组即可创建多光标:
typescript
// 在每一行行首创建一个光标
function cursorAtEveryLineStart(editor: vscode.TextEditor): void {
const doc = editor.document;
const selections: vscode.Selection[] = [];
for (let line = 0; line < doc.lineCount; line++) {
const pos = new vscode.Position(line, 0);
selections.push(new vscode.Selection(pos, pos));
}
editor.selections = selections; // 一次赋值,全部生效
}选区事件监听
typescript
// 选区变化监听(多光标操作、鼠标拖动都会触发)
context.subscriptions.push(
vscode.window.onDidChangeTextEditorSelection((event) => {
const editor = event.textEditor;
const sels = event.selections;
// 在状态栏显示光标数量
vscode.window.setStatusBarMessage(
`光标数: ${sels.length}`, 2000
);
// 主选区位置变化(导航型插件的常用信号)
const primary = sels[sels.length - 1];
console.log('光标位于', primary.active.line, primary.active.character);
})
);可见范围与滚动定位
visibleRanges:屏幕上可见的行
typescript
// 当前可视区域(可能包含多个范围,如拆分视图)
const visible: readonly vscode.Range[] = editor.visibleRanges;
if (visible.length > 0) {
const topLine = visible[0].start.line;
const bottomLine = visible[0].end.line;
console.log(`当前可见行: ${topLine} - ${bottomLine}`);
}典型应用:滚动加载式插件——只在可见行上渲染装饰,视口外跳过,大幅降低开销。
revealRange:滚动到指定位置
revealRange 带 revealType 参数控制滚动行为:
| revealType | 行为 |
|---|---|
vscode.TextEditorRevealType.Default | 滚动到刚好可见 |
vscode.TextEditorRevealType.InCenter | 目标居中显示 |
vscode.TextEditorRevealType.InCenterIfOutsideViewport | 不在视口内才居中 |
vscode.TextEditorRevealType.AtTop | 目标滚到视口顶部 |
typescript
function jumpToLine(editor: vscode.TextEditor, line: number): void {
const pos = new vscode.Position(line, 0);
const range = new vscode.Range(pos, pos);
// 居中显示目标行
editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
// 设置光标位置
editor.selection = new vscode.Selection(pos, pos);
// 让编辑器获得焦点
editor.show();
}跳到某单词并选中
typescript
function jumpToWord(
editor: vscode.TextEditor,
word: string
): boolean {
const doc = editor.document;
const regex = new RegExp(`\\b${word}\\b`);
for (let line = 0; line < doc.lineCount; line++) {
const text = doc.lineAt(line).text;
const match = regex.exec(text);
if (match) {
const start = new vscode.Position(line, match.index);
const end = new vscode.Position(line, match.index + match[0].length);
const range = new vscode.Range(start, end);
// 滚动到视口内并选中
editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
editor.selection = new vscode.Selection(start, end);
return true;
}
}
return false;
}装饰辅助:高亮当前选区
选区与装饰结合能做出「全文档高亮选中词」的效果——把主选区里的词在全文找出来画上标记:
typescript
// 模块级装饰类型:只创建一次,避免反复 create 造成泄漏
const wordHighlight = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(86, 156, 214, 0.25)',
border: '1px solid rgba(86, 156, 214, 0.6)'
});
// 高亮当前选中单词的所有出现位置
function highlightWordOccurrences(editor: vscode.TextEditor): void {
const decoration = wordHighlight;
const doc = editor.document;
const selection = editor.selection;
// 无选区或选区跨行时不处理
if (selection.isEmpty || !selection.isSingleLine) {
editor.setDecorations(decoration, []);
return;
}
const word = doc.getText(selection);
if (!word.trim()) {
editor.setDecorations(decoration, []);
return;
}
// 全文搜索该词
const regex = new RegExp(
word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'
);
const ranges: vscode.Range[] = [];
let match: RegExpExecArray | null;
while ((match = regex.exec(doc.getText()))) {
ranges.push(new vscode.Range(
doc.positionAt(match.index),
doc.positionAt(match.index + word.length)
));
}
editor.setDecorations(decoration, ranges);
}
// 选区变化时重新高亮
vscode.window.onDidChangeTextEditorSelection((event) => {
highlightWordOccurrences(event.textEditor);
});GutterIconPath:行号区图标
装饰的 gutterIconPath 在行号右侧显示图标,适合标记「这行有书签 / 有断点 / 有错误」:
typescript
// 书签图标装饰类型(支持 dark/light 双主题图标)
const bookmarkDecoration = vscode.window.createTextEditorDecorationType({
gutterIconPath: vscode.Uri.joinPath(
context.extensionUri, 'media', 'bookmark.svg'
),
isWholeLine: true,
backgroundColor: 'rgba(255, 193, 7, 0.15)'
});
// 只对有书签的行应用(整行装饰)
const bookmarkLines = [3, 12, 45];
const ranges = bookmarkLines.map((line) => {
const pos = new vscode.Position(line, 0);
return new vscode.Range(pos, pos);
});
editor.setDecorations(bookmarkDecoration, ranges);gutterIconPath 也可以传入 { dark: Uri, light: Uri } 对象,按主题切换图标。
多光标批量编辑示例
场景:在每行行尾追加分号
typescript
function addSemicolons(editor: vscode.TextEditor): void {
const doc = editor.document;
const edit = new vscode.WorkspaceEdit();
const textEdits: vscode.TextEdit[] = [];
for (let line = 0; line < doc.lineCount; line++) {
const text = doc.lineAt(line).text.trim();
// 跳过空行与已有分号的行
if (!text || text.endsWith(';')) {
continue;
}
// 在行尾位置插入分号
const endPos = new vscode.Position(line, doc.lineAt(line).text.length);
textEdits.push(vscode.TextEdit.insert(endPos, ';'));
}
// 批量应用:一次编辑全部生效
edit.set(doc.uri, textEdits);
vscode.workspace.applyEdit(edit);
}场景:选中多行后统一加前缀
typescript
function prefixLines(editor: vscode.TextEditor, prefix: string): void {
const doc = editor.document;
const textEdits: vscode.TextEdit[] = [];
for (const sel of editor.selections) {
const startLine = sel.start.line;
const endLine = sel.end.line;
for (let line = startLine; line <= endLine; line++) {
textEdits.push(vscode.TextEdit.insert(
new vscode.Position(line, 0), prefix
));
}
}
const edit = new vscode.WorkspaceEdit();
edit.set(doc.uri, textEdits);
vscode.workspace.applyEdit(edit);
}文本替换的三种手段对比
| 手段 | 特点 | 适用 |
|---|---|---|
editor.edit() | 直接改当前编辑器,可链式回调 | 单编辑器小改动 |
WorkspaceEdit | 批量、跨文件,先构建后应用 | 多文件重构 |
| 直接写选区文本 | 替换所有选区内容 | 多光标同步输入 |
typescript
// 多光标同步输入:把每个选区内容替换为相同文本
function replaceAllSelections(editor: vscode.TextEditor, text: string): void {
editor.edit((editBuilder) => {
for (const sel of editor.selections) {
editBuilder.replace(sel, text);
}
});
}完整示例:批量添加日志的注释标记
把选中行统一处理为「注释掉的调试日志」,融合多光标、Range 计算与文本编辑:
typescript
// extension.ts
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
// 命令:给所有选中行首加 // LOG: 前缀
context.subscriptions.push(
vscode.commands.registerCommand('myExt.markLogLines', () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
const doc = editor.document;
const edit = new vscode.WorkspaceEdit();
const inserts: { uri: vscode.Uri; position: vscode.Position }[] = [];
// 遍历所有选区覆盖的行
for (const sel of editor.selections) {
const startLine = sel.start.line;
// 反向选区时 end 可能更靠前,取安全顺序
const endLine = sel.end.line >= startLine
? sel.end.line
: sel.start.line;
for (let line = startLine; line <= endLine; line++) {
const text = doc.lineAt(line).text;
// 已标记的跳过
if (text.trimStart().startsWith('// LOG:')) {
continue;
}
inserts.push({
uri: doc.uri,
position: new vscode.Position(line, 0)
});
}
}
for (const item of inserts) {
edit.insert(item.uri, item.position, '// LOG: ');
}
vscode.workspace.applyEdit(edit);
// 完成后把光标移到第一个被标记的行并居中
if (inserts.length > 0) {
const firstLine = inserts[0].position.line;
const pos = new vscode.Position(firstLine, 0);
editor.revealRange(
new vscode.Range(pos, pos),
vscode.TextEditorRevealType.InCenterIfOutsideViewport
);
}
})
);
}常见问题
| 问题 | 处理 |
|---|---|
| 选区反向(isReversed) | 操作前统一 start/end,或直接使用 anchor/active |
| 多光标赋值不生效 | 用 editor.selections = [...] 整体赋值 |
| revealRange 无反应 | 确认 range 有效,且传入 revealType |
| 修改后光标位置错乱 | 编辑后重新显式设置 selection |
| 大文档滚动卡顿 | 用 visibleRanges 只处理可见行 |
| 批量编辑部分失败 | 检查插入位置是否在文档范围内 |
Position、Range、Selection 是编辑器坐标的语言。选区、滚动、装饰三者配合,就能实现从「定位」到「展示」再到「修改」的完整交互链。