import debounce from 'lodash/debounce'; import { Autowired, Injectable } from '@opensumi/di'; import { BasicEvent, CancellationToken, CancellationTokenSource, Deferred, MaybeNull, OnEvent, URI, WithEventBus, } from '@opensumi/ide-core-browser'; import { languageFeaturesService } from '@opensumi/ide-monaco/lib/browser/monaco-api/languages'; import { DocumentSymbol, SymbolTag } from '@opensumi/monaco-editor-core/esm/vs/editor/common/languages'; import { WorkbenchEditorService } from '../../common'; import { EditorDocumentModelContentChangedEvent, IEditorDocumentModelService } from '../doc-model/types'; @Injectable() export class DocumentSymbolStore extends WithEventBus { @Autowired(IEditorDocumentModelService) editorDocumentModelRegistry: IEditorDocumentModelService; @Autowired(WorkbenchEditorService) editorService: WorkbenchEditorService; private documentSymbols = new Map(); private pendingUpdate = new Set(); private debounced = new Map any>(); private symbolDeferred = new Map>(); constructor() { super(); this.addDispose( languageFeaturesService.documentSymbolProvider.onDidChange(() => { Array.from(this.documentSymbols.keys()).forEach((uriString) => { this.markNeedUpdate(new URI(uriString)); }); }), ); } getDocumentSymbol(uri: URI): INormalizedDocumentSymbol[] | undefined { if (!this.documentSymbols.has(uri.toString())) { this.documentSymbols.set(uri.toString(), undefined); this.createDocumentSymbolCache(uri); } if (this.pendingUpdate.has(uri.toString())) { this.updateDocumentSymbolCache(uri); } return this.documentSymbols.get(uri.toString()); } /** * 等待获取文件 symbol,否则文件搜索一个未打开过的文件 symbols 为空 */ async getDocumentSymbolAsync(uri: URI, token?: CancellationToken): Promise { const uriStr = uri.toString(); if (token) { token.onCancellationRequested(() => { this.symbolDeferred.get(uriStr)?.resolve(); this.symbolDeferred.delete(uriStr); }); } if ((!this.documentSymbols.has(uriStr) || this.pendingUpdate.has(uriStr)) && !this.symbolDeferred.has(uriStr)) { this.symbolDeferred.set(uriStr, new Deferred()); this.updateDocumentSymbolCache(uri); } await this.symbolDeferred.get(uriStr)?.promise; return this.documentSymbols.get(uriStr); } async createDocumentSymbolCache(uri: URI) { this.updateDocumentSymbolCache(uri); } async doUpdateDocumentSymbolCache(uri: URI) { this.pendingUpdate.delete(uri.toString()); const modelRef = await this.editorDocumentModelRegistry.createModelReference(uri); if (!modelRef) { this.symbolDeferred.get(uri.toString())?.resolve(); return; } try { const supports = languageFeaturesService.documentSymbolProvider.all(modelRef.instance.getMonacoModel()); let result: MaybeNull; for (const support of supports) { result = await support.provideDocumentSymbols( modelRef.instance.getMonacoModel(), new CancellationTokenSource().token, ); if (result) { break; } } if (result) { normalizeDocumentSymbols(result, { children: result } as INormalizedDocumentSymbol, uri); } this.documentSymbols.set(uri.toString(), result as INormalizedDocumentSymbol[]); this.eventBus.fire(new DocumentSymbolChangedEvent(uri)); } finally { modelRef.dispose(); } this.symbolDeferred.get(uri.toString())?.resolve(); } updateDocumentSymbolCache(uri: URI) { if (!this.debounced.has(uri.toString())) { this.debounced.set( uri.toString(), debounce(() => this.doUpdateDocumentSymbolCache(uri), 100, { maxWait: 1000 }), ); } this.debounced.get(uri.toString())!(); } @OnEvent(EditorDocumentModelContentChangedEvent) onEditorDocumentModelContentChangedEvent(e: EditorDocumentModelContentChangedEvent) { if (e.payload.changes && e.payload.changes.length > 0) { this.markNeedUpdate(e.payload.uri); } } private markNeedUpdate(uri: URI) { this.pendingUpdate.add(uri.toString()); if (this.isWatching(uri)) { this.updateDocumentSymbolCache(uri); } } private isWatching(uri: URI): boolean { for (const g of this.editorService.editorGroups) { if (g.currentResource && g.currentResource.uri.isEqual(uri)) { return true; } } return false; } } export class DocumentSymbolChangedEvent extends BasicEvent {} export { DocumentSymbol, SymbolTag }; export interface INormalizedDocumentSymbol extends DocumentSymbol { parent?: INormalizedDocumentSymbol | IDummyRoot; children?: INormalizedDocumentSymbol[]; id: string; } export interface IDummyRoot { children?: INormalizedDocumentSymbol[]; } function normalizeDocumentSymbols( documentSymbols: DocumentSymbol[], parent: INormalizedDocumentSymbol | IDummyRoot, uri: URI, ): INormalizedDocumentSymbol[] { documentSymbols.forEach((documentSymbol, index) => { const symbol = documentSymbol as INormalizedDocumentSymbol; symbol.parent = parent; symbol.id = getSymbolId(uri, symbol, index); if (documentSymbol.children && documentSymbol.children.length > 0) { normalizeDocumentSymbols(documentSymbol.children, documentSymbol as INormalizedDocumentSymbol, uri); } }); return documentSymbols as INormalizedDocumentSymbol[]; } function getSymbolId(uri: URI, symbol: INormalizedDocumentSymbol, index: number) { const symbolNameList: string[] = [symbol.name]; while (symbol.parent) { const parent = symbol.parent as INormalizedDocumentSymbol; // dummyRoot if (!parent.name) { break; } symbolNameList.unshift(parent.name); symbol = parent; } return `${uri.toString()}__${symbolNameList.join('-')}__${index}`; }