onDidChangeSemanticTokens无法稳定触发provideDocumentSemanticTokens问题排查
问题描述
开发VS Code扩展时实现了SemanticTokenProvider,编辑内容被服务器解析时功能正常。但为了优化性能,实现了TextEdits批量处理逻辑:短时间内的编辑会被合并,新编辑会取消待发送的服务器消息,合并后重新入队。
由于SemanticTokenProvider会在每次编辑后自动触发,但有时实际编辑已从队列移除,导致Provider基于未同步到服务器的旧数据执行。
为解决此问题,创建了一个在TextEdits实际发送到服务器时触发的事件,尝试通过该事件触发关联onDidChangeSemanticTokens的vscode.EventEmitter,但并未触发Provider。测试命令手动触发时能正常更新语义令牌,但从自定义事件中调用相同的fire()逻辑却无效——确认回调已触发,但provideDocumentSemanticTokens断点未进入。
核心实现代码如下:
// 事件定义 private mOnSentTextEditsEventEmitter: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); private mOnSentTextEditsEvent: vscode.Event<void> = this.mOnSentTextEditsEventEmitter.event; // 文档变更回调 vscode.workspace.onDidChangeTextDocument( (event: vscode.TextDocumentChangeEvent) => { // 编辑入队逻辑 this.mQueuedNotification.then( () => { this.mTextEdits = []; this.mQueuedNotification = null; this.mOnSentTextEditsEventEmitter.fire(); }); }) // 注册回调方法 RegisterOnSentTextEdits(inThis: unknown, inCallback: () => void) { this.mOnSentTextEditsEvent.call(inThis, inCallback); } // 注册回调触发语义令牌更新 file_watcher.RegisterOnSentTextEdits(semantic_highlighter_provider_instance, () => { semantic_highlighter_provider_instance.mOnDidchangeSemanticTokens.fire(); }) // 可正常触发的测试命令 let update_semantic_tokens = vscode.commands.registerCommand('update_semantic_tokens', () => { semantic_highlighter_provider_instance.mOnDidchangeSemanticTokens.fire(); })
错误分析
- 事件订阅方式错误:
RegisterOnSentTextEdits方法中使用this.mOnSentTextEditsEvent.call(inThis, inCallback)是错误的。VS Code的Event类型本身就是一个订阅函数,直接传入回调即可完成订阅,不需要用call绑定this。这种错误绑定会导致回调并未真正注册到事件上,自然不会触发后续的语义令牌更新逻辑。 - 上下文绑定逻辑冗余:如果回调需要特定的
this指向,应该在调用时通过箭头函数或bind处理,而非修改事件函数的this指向。
修复方案
1. 修正事件订阅方法
修改RegisterOnSentTextEdits,直接调用事件函数完成订阅:
RegisterOnSentTextEdits(inCallback: () => void) { // 直接订阅事件,无需额外绑定this this.mOnSentTextEditsEvent(inCallback); }
如果需要保留上下文绑定需求,由调用方自行处理即可:
// 调用时用箭头函数确保上下文正确 file_watcher.RegisterOnSentTextEdits(() => { semantic_highlighter_provider_instance.mOnDidchangeSemanticTokens.fire(); });
2. 确保语义令牌事件正确关联
确认SemanticTokenProvider的onDidChangeSemanticTokens属性正确指向自定义的事件发射器:
class SemanticHighlighterProvider implements vscode.DocumentSemanticTokensProvider { public readonly onDidChangeSemanticTokens: vscode.Event<void>; private mOnDidchangeSemanticTokens: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); constructor() { this.onDidChangeSemanticTokens = this.mOnDidchangeSemanticTokens.event; } // ... 其他Provider实现方法 }
3. 精准触发文档更新(可选优化)
如果希望只更新目标文档的语义令牌,可修改事件携带文档信息:
// 修改事件定义为携带文档参数 private mOnSentTextEditsEventEmitter: vscode.EventEmitter<vscode.TextDocument> = new vscode.EventEmitter<vscode.TextDocument>(); private mOnSentTextEditsEvent: vscode.Event<vscode.TextDocument> = this.mOnSentTextEditsEventEmitter.event; // 发送编辑后触发时传入当前文档 vscode.workspace.onDidChangeTextDocument( (event: vscode.TextDocumentChangeEvent) => { this.mQueuedNotification.then( () => { this.mTextEdits = []; this.mQueuedNotification = null; this.mOnSentTextEditsEventEmitter.fire(event.document); }); }) // 注册回调时针对特定文档触发更新 file_watcher.RegisterOnSentTextEdits((doc) => { semantic_highlighter_provider_instance.mOnDidchangeSemanticTokens.fire(doc); });
同时更新Provider的事件类型:
class SemanticHighlighterProvider implements vscode.DocumentSemanticTokensProvider { public readonly onDidChangeSemanticTokens: vscode.Event<vscode.TextDocument | undefined>; private mOnDidchangeSemanticTokens: vscode.EventEmitter<vscode.TextDocument | undefined> = new vscode.EventEmitter<vscode.TextDocument | undefined>(); constructor() { this.onDidChangeSemanticTokens = this.mOnDidchangeSemanticTokens.event; } // ... 其他Provider实现方法 }
验证方法
修复后,当批量编辑发送到服务器时,自定义事件会正确触发回调,调用mOnDidchangeSemanticTokens.fire()后,VS Code会重新调用provideDocumentSemanticTokens方法获取最新语义令牌,断点可正常命中。
内容的提问来源于stack exchange,提问作者Ricardo Desmedt

