选择范围与编辑链
双击选中(Selection Range)与文档变更监听(编辑链)是编辑器交互的底层能力:智能扩选、多级选中、监听编辑并联动。
注册选择范围 Provider
registerSelectionRangeProvider 提供智能选择范围:
typescript
import * as vscode from 'vscode';
vscode.languages.registerSelectionRangeProvider(
'plaintext',
{
provideSelectionRanges(
document: vscode.TextDocument,
positions: vscode.Position[], // 多个光标位置
token: vscode.CancellationToken
): vscode.SelectionRange[] {
// 返回选择范围(从内到外)
const position = positions[0];
return buildSelectionRanges(document, position);
}
}
);SelectionRange 层级
选择范围是从内到外的嵌套结构:
typescript
function buildSelectionRanges(
document: vscode.TextDocument,
position: vscode.Position
): vscode.SelectionRange[] {
const line = position.line;
const text = document.lineAt(line).text;
// 1. 单词级(内层)
const wordRange = document.getWordRangeAtPosition(position);
if (!wordRange) {
return [];
}
const wordSelection = new vscode.SelectionRange(wordRange);
// 2. 引号内容级
const quoted = matchQuoted(text, position.character);
let inner = wordSelection;
if (quoted) {
const quotedSelection = new vscode.SelectionRange(quoted, wordSelection);
inner = quotedSelection;
}
// 3. 整行级(外层)
const lineRange = new vscode.Range(
new vscode.Position(line, 0),
new vscode.Range(
line, 0, line, text.length
).end
);
const lineSelection = new vscode.SelectionRange(lineRange, inner);
return [lineSelection];
}
// 匹配引号内的范围
function matchQuoted(
text: string,
character: number
): vscode.Range | undefined {
const before = text.slice(0, character);
const quoteIndex = before.lastIndexOf('"');
const quoteClose = text.indexOf('"', character);
if (quoteIndex !== -1 && quoteClose !== -1) {
return new vscode.Range(0, quoteIndex, 0, quoteClose + 1);
}
return undefined;
}SelectionRange 嵌套
typescript
// 从内到外链接:子 → 父
const word = new vscode.SelectionRange(wordRange);
const line = new vscode.SelectionRange(lineRange, word); // 父含子
const block = new vscode.SelectionRange(blockRange, line);
return [block];双击扩选行为
| 点击次数 | 选择范围 |
|---|---|
| 第 1 次双击 | 最内层(单词) |
| 第 2 次双击 | 上一层(引号内容/表达式) |
| 第 3 次双击 | 整行 |
范围格式化 Provider
registerDocumentRangeFormattingEditProvider 注册选区格式化:
typescript
vscode.languages.registerDocumentRangeFormattingEditProvider(
'plaintext',
{
provideDocumentRangeFormattingEdits(
document,
range,
options,
token
): vscode.TextEdit[] {
// 只格式化选中区域
const selectedText = document.getText(range);
// 对选中内容做缩进重排
const formatted = reindentText(selectedText, options);
// 返回替换编辑
return [
vscode.TextEdit.replace(range, formatted)
];
}
}
);选区缩进格式化
typescript
function reindentText(
text: string,
options: vscode.FormattingOptions
): string {
const lines = text.split('\n');
let depth = 0;
return lines.map((line) => {
const trimmed = line.trim();
if (trimmed === '') {
return '';
}
// 结束括号减少缩进
if (trimmed.startsWith('}')) {
depth = Math.max(0, depth - 1);
}
const indent = ' '.repeat(depth * options.tabSize);
const result = indent + trimmed;
// 开始括号增加缩进
if (trimmed.endsWith('{')) {
depth++;
}
return result;
}).join('\n');
}onDidChangeTextDocument 编辑链
监听文档变更,构建编辑链(顺序处理编辑):
typescript
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
// 文档变更监听
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument((event) => {
const document = event.document;
// 只处理目标语言
if (document.languageId !== 'plaintext') {
return;
}
// 遍历本次变更
event.contentChanges.forEach((change) => {
console.log(`变更行: ${change.range.start.line}`);
console.log(`插入文本: ${change.text}`);
console.log(`删除长度: ${change.rangeLength}`);
});
// 文档当前内容已更新
const currentText = document.getText();
})
);
}TextDocumentContentChangeEvent
| 属性 | 说明 |
|---|---|
range | 变更范围 |
rangeOffset | 变更偏移 |
rangeLength | 变更长度 |
text | 插入的文本 |
编辑链:自动补全场景
监听编辑实时响应:
typescript
class AutoEditListener {
constructor(private readonly disposable: vscode.Disposable) {}
// 自动补全括号
private handleChange(event: vscode.TextDocumentChangeEvent) {
for (const change of event.contentChanges) {
// 输入 ( 时自动补 )
if (change.text === '(') {
this.insertMatching(event.document, change, ')');
}
// 输入 " 时自动补 "
if (change.text === '"') {
this.insertMatching(event.document, change, '"');
}
}
}
private async insertMatching(
document: vscode.TextDocument,
change: vscode.TextDocumentContentChangeEvent,
closing: string
) {
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document.uri.toString() !== document.uri.toString()) {
return;
}
// 在光标处插入闭合字符
const position = change.range.end;
await editor.edit((builder) => {
builder.insert(position, closing);
});
// 光标移到中间
const newPosition = position.translate(0, 1);
editor.selection = new vscode.Selection(newPosition, newPosition);
}
}
export function activate(context: vscode.ExtensionContext) {
const listener = new AutoEditListener(
vscode.workspace.onDidChangeTextDocument((event) => {
listener.handleChange(event);
})
);
context.subscriptions.push(listener);
}防抖编辑处理
频繁输入时合并处理(防抖):
typescript
class DebouncedListener {
private timer: NodeJS.Timeout | undefined;
private pendingDoc: vscode.TextDocument | undefined;
handleChange(event: vscode.TextDocumentChangeEvent) {
// 记录待处理文档
this.pendingDoc = event.document;
// 500ms 内合并多次变更
clearTimeout(this.timer);
this.timer = setTimeout(() => {
if (this.pendingDoc) {
this.processDocument(this.pendingDoc);
}
}, 500);
}
private processDocument(document: vscode.TextDocument) {
// 统一处理(如诊断、格式化)
console.log(`处理文档: ${document.fileName}`);
}
}完整示例:智能缩进编辑链
typescript
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
// 监听编辑:自动缩进
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument((event) => {
const doc = event.document;
for (const change of event.contentChanges) {
// 输入换行后自动缩进
if (change.text.includes('\n')) {
autoIndent(doc, change);
}
}
})
);
}
function autoIndent(
document: vscode.TextDocument,
change: vscode.TextDocumentContentChangeEvent
) {
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document.uri.toString() !== document.uri.toString()) {
return;
}
// 取上一行
const prevLine = change.range.start.line - 1;
if (prevLine < 0) {
return;
}
const prevText = document.lineAt(prevLine).text;
// 计算当前缩进深度
let depth = 0;
for (const char of prevText) {
if (char === '{' || char === '(' || char === '[') depth++;
if (char === '}' || char === ')' || char === ']') depth--;
}
// 上一行结尾是 { 则额外缩进
if (prevText.trimEnd().endsWith('{')) {
depth++;
}
// 插入缩进
const indent = ' '.repeat(Math.max(0, depth));
editor.edit((builder) => {
builder.insert(change.range.end, indent);
});
}编辑链优化
| 场景 | 处理 |
|---|---|
| 高频变更 | 防抖合并处理 |
| 递归监听 | 避免编辑触发监听再编辑 |
| 撤销一致性 | 编辑应可撤销 |
| 性能 | 只处理目标语言 |
常见问题
| 问题 | 处理 |
|---|---|
| 双击不扩选 | 检查 SelectionRange 嵌套 |
| 选区格式化失效 | 确认注册 range 版本 |
| 监听不触发 | 确认事件绑定 |
| 无限循环 | 编辑前加条件判断 |
选择范围与编辑链是编辑体验的底层支撑,掌握后可实现智能扩选、自动缩进等细腻交互。