import debounce from 'lodash/debounce'; import { Autowired, Injectable } from '@opensumi/di'; import { CommandService, EDITOR_COMMANDS, Emitter, Event, IPosition, IRange, LRUMap, MaybeNull, OnEvent, URI, WithEventBus, getSymbolIcon, path, } from '@opensumi/ide-core-browser'; import { LabelService } from '@opensumi/ide-core-browser/lib/services'; import { FileStat } from '@opensumi/ide-file-service/lib/common'; import { IFileServiceClient } from '@opensumi/ide-file-service/lib/common/file-service-client'; import { IWorkspaceService } from '@opensumi/ide-workspace/lib/common/workspace.interface'; import { IEditor } from '../../common'; import { EditorSelectionChangeEvent, IBreadCrumbPart, IBreadCrumbProvider } from '../types'; import { DocumentSymbolChangedEvent, DocumentSymbolStore, INormalizedDocumentSymbol } from './document-symbol'; const { Path } = path; @Injectable() export class DefaultBreadCrumbProvider extends WithEventBus implements IBreadCrumbProvider { private _onDidUpdateBreadCrumb = new Emitter(); public onDidUpdateBreadCrumb: Event = this._onDidUpdateBreadCrumb.event; @Autowired(IFileServiceClient) fileServiceClient: IFileServiceClient; @Autowired(IWorkspaceService) workspaceService: IWorkspaceService; @Autowired(LabelService) labelService: LabelService; @Autowired(CommandService) commandService: CommandService; @Autowired() documentSymbolStore: DocumentSymbolStore; private debouncedFireUriEvent = new Map any>(); private cachedBreadCrumb = new LRUMap(200, 100); handlesUri(uri: URI): boolean { return this.fileServiceClient.handlesScheme(uri.scheme); } provideBreadCrumbForUri(uri: URI, editor: MaybeNull): IBreadCrumbPart[] { return this.getFileParts(uri).concat(this.getDocumentSymbolParts(uri, editor)); } private getFileParts(uri: URI): IBreadCrumbPart[] { const workspaceRoot: URI | undefined = this.workspaceService.workspace ? new URI(this.workspaceService.workspace.uri) : undefined; let root: URI; let relativePaths: path.Path; if (workspaceRoot && workspaceRoot.isEqualOrParent(uri)) { root = workspaceRoot; relativePaths = workspaceRoot.relative(uri)!; } else { root = URI.from({ scheme: uri.scheme, authority: uri.authority, }); relativePaths = uri.path; } let p = root.path; const result: IBreadCrumbPart[] = []; for (const r of relativePaths .toString() .split(Path.separator) .filter((p) => !!p)) { p = p.join(r); const u = root.withPath(p); result.push(this.createFilePartBreadCrumbUri(u, !u.isEqual(uri))); } return result; } private createFilePartBreadCrumbUri(uri: URI, isDirectory: boolean): IBreadCrumbPart { const uriString = uri.toString(); if (this.cachedBreadCrumb.has(uriString)) { return this.cachedBreadCrumb.get(uriString)!; } const res: IBreadCrumbPart = { name: uri.path.base, uri, icon: this.labelService.getIcon(uri, { isDirectory }), getSiblings: async () => { const parentDir = URI.from({ scheme: uri.scheme, }).withPath(uri.path.dir); const stat = await this.fileServiceClient.getFileStat(parentDir.toString()); const parts: IBreadCrumbPart[] = []; let currentIndex = -1; if (stat && stat.children && stat.children.length > 0) { sortByNumeric(stat.children).forEach((file, i) => { parts.push(this.createFilePartBreadCrumbUri(new URI(file.uri), file.isDirectory)); if (currentIndex === -1 && uri.toString() === file.uri) { currentIndex = i; } }); } return { parts, currentIndex, }; }, }; if (isDirectory) { res.getChildren = async () => { const stat = await this.fileServiceClient.getFileStat(uri.toString()); const parts: IBreadCrumbPart[] = []; if (stat && stat.children && stat.children.length > 0) { sortByNumeric(stat.children).forEach((file, i) => { parts.push(this.createFilePartBreadCrumbUri(new URI(file.uri), file.isDirectory)); }); } return parts; }; } else { res.onClick = () => { this.commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, uri); }; } this.cachedBreadCrumb.set(uriString, res); return res; } private getDocumentSymbolParts(uri: URI, editor: MaybeNull): IBreadCrumbPart[] { if (!editor) { return []; } const symbols = this.documentSymbolStore.getDocumentSymbol(uri); if (symbols && symbols.length > 0) { const currentSymbols = findCurrentDocumentSymbol(symbols, editor.monacoEditor.getPosition()); if (currentSymbols.length > 0) { return currentSymbols.map((symbol) => this.createFromDocumentSymbol(symbol, editor)); } else { return [ { name: '...', getSiblings: () => ({ parts: symbols .sort((a, b) => sortByRangeStart(a.range, b.range)) .map((symbol) => this.createFromDocumentSymbol(symbol, editor)), currentIndex: -1, }), }, ]; } } else { return []; } } private createFromDocumentSymbol(symbol: INormalizedDocumentSymbol, editor: IEditor): IBreadCrumbPart { const res: IBreadCrumbPart = { name: symbol.name, icon: getSymbolIcon(symbol.kind), isSymbol: true, onClick: () => { editor.setSelection({ startColumn: symbol.range.startColumn, endColumn: symbol.range.startColumn, startLineNumber: symbol.range.startLineNumber, endLineNumber: symbol.range.startLineNumber, }); editor.monacoEditor.revealRangeInCenter(symbol.range); editor.monacoEditor.focus(); }, }; if (symbol.parent) { res.getSiblings = () => ({ parts: symbol .parent!.children!.sort((a, b) => sortByRangeStart(a.range, b.range)) .map((symbol) => this.createFromDocumentSymbol(symbol, editor)), currentIndex: symbol.parent!.children!.indexOf(symbol), }); } if (symbol.children && symbol.children.length > 0) { res.getChildren = () => symbol .children!.sort((a, b) => sortByRangeStart(a.range, b.range)) .map((symbol) => this.createFromDocumentSymbol(symbol, editor)); } return res; } @OnEvent(DocumentSymbolChangedEvent) onDocumentSymbolChangedEvent(e: DocumentSymbolChangedEvent) { this.notifyUpdate(e.payload); } @OnEvent(EditorSelectionChangeEvent) onEditorSelectionChangeEvent(e: EditorSelectionChangeEvent) { this.notifyUpdate(e.payload.editorUri); } private notifyUpdate(uri: URI) { if (!this.debouncedFireUriEvent.has(uri.toString())) { this.debouncedFireUriEvent.set( uri.toString(), debounce( () => { this._onDidUpdateBreadCrumb.fire(uri); }, 100, { maxWait: 1000 }, ), ); } this.debouncedFireUriEvent.get(uri.toString())!(); } } export function findCurrentDocumentSymbol( documentSymbols: INormalizedDocumentSymbol[], position: MaybeNull, ): INormalizedDocumentSymbol[] { const result: INormalizedDocumentSymbol[] = []; if (!position) { return result; } let toFindIn: INormalizedDocumentSymbol[] | undefined = documentSymbols; while (toFindIn && toFindIn.length > 0) { let found = false; for (const documentSymbol of toFindIn) { if (positionInRange(position, documentSymbol.range)) { result.push(documentSymbol); toFindIn = documentSymbol.children; found = true; break; } } if (!found) { break; } } return result; } function sortByNumeric(files: FileStat[]): FileStat[] { return files.sort((a: FileStat, b: FileStat) => { if ((a.isDirectory && b.isDirectory) || (!a.isDirectory && !b.isDirectory)) { // numeric 参数确保数字为第一排序优先级 const nameA = new URI(a.uri).path.name; const nameB = new URI(b.uri).path.name; return nameA.localeCompare(nameB, 'kn', { numeric: true }); } else if (a.isDirectory && !b.isDirectory) { return -1; } else if (!a.isDirectory && b.isDirectory) { return 1; } else { return 0; } }); } function positionInRange(pos: IPosition, range: IRange): boolean { if (pos.lineNumber < range.startLineNumber) { return false; } else if (pos.lineNumber === range.startLineNumber) { return pos.column >= range.startColumn; } else if (pos.lineNumber < range.endLineNumber) { return true; } else if (pos.lineNumber === range.endLineNumber) { return pos.column <= range.endColumn; } else { return false; } } function sortByRangeStart(rangeA: IRange, rangeB: IRange) { if (rangeA.startLineNumber > rangeB.startLineNumber) { return 1; } else if (rangeA.startLineNumber < rangeB.startLineNumber) { return -1; } else { if (rangeA.startColumn === rangeB.startColumn) { return 0; } return rangeA.startColumn > rangeB.startColumn ? 1 : -1; } }