diff --git a/packages/language-server/src/plugins/PluginHost.ts b/packages/language-server/src/plugins/PluginHost.ts index f251582c0..949108808 100644 --- a/packages/language-server/src/plugins/PluginHost.ts +++ b/packages/language-server/src/plugins/PluginHost.ts @@ -35,7 +35,8 @@ import { TextEdit, WorkspaceEdit, InlayHint, - WorkspaceSymbol + WorkspaceSymbol, + DocumentSymbol } from 'vscode-languageserver'; import { DocumentManager, getNodeIfIsInHTMLStartTag } from '../lib/documents'; import { Logger } from '../logger'; @@ -307,6 +308,7 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { if (cancellationToken.isCancellationRequested) { return []; } + return flatten( await this.execute( 'getDocumentSymbols', @@ -317,6 +319,63 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { ); } + private comparePosition(pos1: Position, pos2: Position) { + if (pos1.line < pos2.line) return -1; + if (pos1.line > pos2.line) return 1; + if (pos1.character < pos2.character) return -1; + if (pos1.character > pos2.character) return 1; + return 0; + } + + private rangeContains(parent: Range, child: Range) { + return ( + this.comparePosition(parent.start, child.start) <= 0 && + this.comparePosition(child.end, parent.end) <= 0 + ); + } + + async getHierarchicalDocumentSymbols( + textDocument: TextDocumentIdentifier, + cancellationToken: CancellationToken + ): Promise { + const flat = await this.getDocumentSymbols(textDocument, cancellationToken); + const symbols = flat + .map((s) => + DocumentSymbol.create( + s.name, + undefined, + s.kind, + s.location.range, + s.location.range, + [] + ) + ) + .sort((a, b) => { + const start = this.comparePosition(a.range.start, b.range.start); + if (start !== 0) return start; + return this.comparePosition(b.range.end, a.range.end); + }); + + const stack: DocumentSymbol[] = []; + const roots: DocumentSymbol[] = []; + + for (const node of symbols) { + while (stack.length > 0 && !this.rangeContains(stack.at(-1)!.range, node.range)) { + stack.pop(); + } + + if (stack.length > 0) { + stack.at(-1)!.children!.push(node); + } else { + roots.push(node); + } + + stack.push(node); + } + + return roots; + } + async getDefinitions( textDocument: TextDocumentIdentifier, position: Position diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index ab5475840..fa7f02d95 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -427,9 +427,16 @@ export function startServer(options?: LSOptions) { connection.onColorPresentation((evt) => pluginHost.getColorPresentations(evt.textDocument, evt.range, evt.color) ); - connection.onDocumentSymbol((evt, cancellationToken) => - pluginHost.getDocumentSymbols(evt.textDocument, cancellationToken) - ); + connection.onDocumentSymbol((evt, cancellationToken) => { + if ( + configManager.getClientCapabilities()?.textDocument?.documentSymbol + ?.hierarchicalDocumentSymbolSupport + ) { + return pluginHost.getHierarchicalDocumentSymbols(evt.textDocument, cancellationToken); + } else { + return pluginHost.getDocumentSymbols(evt.textDocument, cancellationToken); + } + }); connection.onDefinition((evt) => pluginHost.getDefinitions(evt.textDocument, evt.position)); connection.onReferences((evt, cancellationToken) => pluginHost.findReferences(evt.textDocument, evt.position, evt.context, cancellationToken)