From b3dcd3aeb343953646285d06f3e6ba66ab6f10e8 Mon Sep 17 00:00:00 2001 From: Dimas Firmansyah Date: Fri, 8 Aug 2025 03:51:48 +0700 Subject: [PATCH 1/4] returns DocumentSymbol[] on textDocument/documentSymbol --- .../src/lib/documents/DocumentMapper.ts | 10 +- .../language-server/src/plugins/PluginHost.ts | 7 +- .../src/plugins/css/CSSPlugin.ts | 24 ++-- .../src/plugins/html/HTMLPlugin.ts | 8 +- .../language-server/src/plugins/interfaces.ts | 3 +- .../plugins/typescript/TypeScriptPlugin.ts | 111 ++++++++---------- .../typescript/TypescriptPlugin.test.ts | 6 +- 7 files changed, 80 insertions(+), 89 deletions(-) diff --git a/packages/language-server/src/lib/documents/DocumentMapper.ts b/packages/language-server/src/lib/documents/DocumentMapper.ts index fe79fca5a..3c1c8ad43 100644 --- a/packages/language-server/src/lib/documents/DocumentMapper.ts +++ b/packages/language-server/src/lib/documents/DocumentMapper.ts @@ -12,7 +12,8 @@ import { SelectionRange, TextEdit, InsertReplaceEdit, - Location + Location, + DocumentSymbol } from 'vscode-languageserver'; import { TagInformation, offsetAt, positionAt, getLineOffsets } from './utils'; import { Logger } from '../../logger'; @@ -353,9 +354,10 @@ export function mapColorPresentationToOriginal( export function mapSymbolInformationToOriginal( fragment: Pick, - info: SymbolInformation -): SymbolInformation { - return { ...info, location: mapObjWithRangeToOriginal(fragment, info.location) }; + info: DocumentSymbol +): DocumentSymbol { + const range = mapRangeToOriginal(fragment, info.range); + return { ...info, range, selectionRange: range }; } export function mapLocationLinkToOriginal( diff --git a/packages/language-server/src/plugins/PluginHost.ts b/packages/language-server/src/plugins/PluginHost.ts index f251582c0..04ecb6b9f 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'; @@ -298,7 +299,7 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { async getDocumentSymbols( textDocument: TextDocumentIdentifier, cancellationToken: CancellationToken - ): Promise { + ): Promise { const document = this.getDocument(textDocument.uri); // VSCode requested document symbols twice for the outline view and the sticky scroll @@ -308,7 +309,7 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { return []; } return flatten( - await this.execute( + await this.execute( 'getDocumentSymbols', [document, cancellationToken], ExecuteMode.Collect, diff --git a/packages/language-server/src/plugins/css/CSSPlugin.ts b/packages/language-server/src/plugins/css/CSSPlugin.ts index f89a6285e..5e7f3278e 100644 --- a/packages/language-server/src/plugins/css/CSSPlugin.ts +++ b/packages/language-server/src/plugins/css/CSSPlugin.ts @@ -50,7 +50,7 @@ import { getIdClassCompletion } from './features/getIdClassCompletion'; import { AttributeContext, getAttributeContextAtPosition } from '../../lib/documents/parseHtml'; import { StyleAttributeDocument } from './StyleAttributeDocument'; import { getDocumentContext } from '../documentContext'; -import { FoldingRange, FoldingRangeKind } from 'vscode-languageserver-types'; +import { DocumentSymbol, FoldingRange, FoldingRangeKind } from 'vscode-languageserver-types'; import { indentBasedFoldingRangeForTag } from '../../lib/foldingRange/indentFolding'; import { wordHighlightForTag } from '../../lib/documentHighlight/wordHighlight'; import { isNotNullOrUndefined, urlToPath } from '../../utils'; @@ -362,7 +362,7 @@ export class CSSPlugin .map((colorPres) => mapColorPresentationToOriginal(cssDocument, colorPres)); } - getDocumentSymbols(document: Document): SymbolInformation[] { + getDocumentSymbols(document: Document): DocumentSymbol[] { if (!this.featureEnabled('documentColors')) { return []; } @@ -373,20 +373,14 @@ export class CSSPlugin return []; } - return this.getLanguageService(extractLanguage(cssDocument)) - .findDocumentSymbols(cssDocument, cssDocument.stylesheet) - .map((symbol) => { - if (!symbol.containerName) { - return { - ...symbol, - // TODO: this could contain other things, e.g. style.myclass - containerName: 'style' - }; - } + function mapSymbol(symbol: DocumentSymbol) { + symbol.children = symbol.children?.map(mapSymbol); + return mapSymbolInformationToOriginal(cssDocument, symbol); + } - return symbol; - }) - .map((symbol) => mapSymbolInformationToOriginal(cssDocument, symbol)); + return this.getLanguageService(extractLanguage(cssDocument)) + .findDocumentSymbols2(cssDocument, cssDocument.stylesheet) + .map(mapSymbol); } getFoldingRanges(document: Document): FoldingRange[] { diff --git a/packages/language-server/src/plugins/html/HTMLPlugin.ts b/packages/language-server/src/plugins/html/HTMLPlugin.ts index 2e900f626..fe7a85002 100644 --- a/packages/language-server/src/plugins/html/HTMLPlugin.ts +++ b/packages/language-server/src/plugins/html/HTMLPlugin.ts @@ -10,7 +10,6 @@ import { CompletionList, Hover, Position, - SymbolInformation, CompletionItem, CompletionItemKind, TextEdit, @@ -19,7 +18,8 @@ import { LinkedEditingRanges, CompletionContext, FoldingRange, - DocumentHighlight + DocumentHighlight, + DocumentSymbol } from 'vscode-languageserver'; import { DocumentManager, @@ -300,7 +300,7 @@ export class HTMLPlugin return isInsideMoustacheTag(document.getText(), node.start, offset); } - getDocumentSymbols(document: Document): SymbolInformation[] { + getDocumentSymbols(document: Document): DocumentSymbol[] { if (!this.featureEnabled('documentSymbols')) { return []; } @@ -310,7 +310,7 @@ export class HTMLPlugin return []; } - return this.lang.findDocumentSymbols(document, html); + return this.lang.findDocumentSymbols2(document, html); } rename(document: Document, position: Position, newName: string): WorkspaceEdit | null { diff --git a/packages/language-server/src/plugins/interfaces.ts b/packages/language-server/src/plugins/interfaces.ts index c050b2387..9614366cd 100644 --- a/packages/language-server/src/plugins/interfaces.ts +++ b/packages/language-server/src/plugins/interfaces.ts @@ -22,6 +22,7 @@ import { DefinitionLink, Diagnostic, DocumentHighlight, + DocumentSymbol, FoldingRange, FormattingOptions, Hover, @@ -97,7 +98,7 @@ export interface DocumentSymbolsProvider { getDocumentSymbols( document: Document, cancellationToken?: CancellationToken - ): Resolvable; + ): Resolvable; } export interface DefinitionsProvider { diff --git a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts index 1b5781699..f0418c5d7 100644 --- a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts +++ b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts @@ -12,6 +12,7 @@ import { DefinitionLink, Diagnostic, DocumentHighlight, + DocumentSymbol, FileChangeType, FoldingRange, Hover, @@ -35,6 +36,7 @@ import { Document, DocumentManager, getTextInRange, + mapRangeToOriginal, mapSymbolInformationToOriginal } from '../../lib/documents'; import { LSConfigManager, LSTypescriptConfig } from '../../ls-config'; @@ -247,7 +249,7 @@ export class TypeScriptPlugin async getDocumentSymbols( document: Document, cancellationToken?: CancellationToken - ): Promise { + ): Promise { if (!this.featureEnabled('documentSymbols')) { return []; } @@ -258,101 +260,92 @@ export class TypeScriptPlugin return []; } + const hierarchy = + this.configManager.getClientCapabilities()?.textDocument?.documentSymbol + ?.hierarchicalDocumentSymbolSupport ?? false; const navTree = lang.getNavigationTree(tsDoc.filePath); - const symbols: SymbolInformation[] = []; - collectSymbols(navTree, undefined, (symbol) => symbols.push(symbol)); + const symbols: DocumentSymbol[] = []; - const topContainerName = symbols[0].name; - const result: SymbolInformation[] = []; - - for (let symbol of symbols.slice(1)) { - if (symbol.containerName === topContainerName) { - symbol.containerName = 'script'; - } - - symbol = mapSymbolInformationToOriginal(tsDoc, symbol); + function collectSymbols(tree: NavigationTree, cb: (symbol: DocumentSymbol) => void) { + const start = tree.spans[0]; + const end = tree.spans[tree.spans.length - 1]; + if (!(start && end)) return next(); + + let name = tree.text; + const kind = symbolKindFromString(tree.kind); + const range = mapRangeToOriginal( + tsDoc, + Range.create( + tsDoc.positionAt(start.start), + tsDoc.positionAt(end.start + end.length) + ) + ); + const children: DocumentSymbol[] = []; if ( - symbol.location.range.start.line < 0 || - symbol.location.range.end.line < 0 || - isZeroLengthRange(symbol.location.range) || - symbol.name.startsWith('__sveltets_') + range.start.line < 0 || + range.end.line < 0 || + isZeroLengthRange(range) || + name.startsWith('__sveltets_') ) { - continue; + return next(); } if ( - (symbol.kind === SymbolKind.Property || symbol.kind === SymbolKind.Method) && - !isInScript(symbol.location.range.start, document) + (kind === SymbolKind.Property || kind === SymbolKind.Method) && + !isInScript(range.start, document) ) { if ( - symbol.name === 'props' && - document.getText().charAt(document.offsetAt(symbol.location.range.start)) !== - 'p' + name === 'props' && + document.getText().charAt(document.offsetAt(range.start)) !== 'p' ) { // This is the "props" of a generated component constructor - continue; + return next(); } - const node = tsDoc.svelteNodeAt(symbol.location.range.start); + const node = tsDoc.svelteNodeAt(range.start); if ( (node && (isAttributeName(node) || isAttributeShorthand(node))) || isEventHandler(node) ) { // This is a html or component property, they are not treated as a new symbol // in JSX and so we do the same for the new transformation. - continue; + return next(); } } - if (symbol.name === '') { - let name = getTextInRange(symbol.location.range, document.getText()).trimLeft(); + if (name === '') { + name = getTextInRange(range, document.getText()).trimStart(); if (name.length > 50) { name = name.substring(0, 50) + '...'; } - symbol.name = name; } - if (symbol.name.startsWith('$$_')) { - if (!symbol.name.includes('$on')) { - continue; + if (name.startsWith('$$_')) { + if (!name.includes('$on')) { + return next(); } // on:foo={() => ''} -> $on("foo") callback - symbol.name = symbol.name.substring(symbol.name.indexOf('$on')); + name = name.substring(name.indexOf('$on')); } - result.push(symbol); - } + const symbol = DocumentSymbol.create(name, undefined, kind, range, range, children); - return result; + cb(symbol); + if (hierarchy) cb = (s) => children.push(s); + next(); - function collectSymbols( - tree: NavigationTree, - container: string | undefined, - cb: (symbol: SymbolInformation) => void - ) { - const start = tree.spans[0]; - const end = tree.spans[tree.spans.length - 1]; - if (start && end) { - cb( - SymbolInformation.create( - tree.text, - symbolKindFromString(tree.kind), - Range.create( - tsDoc.positionAt(start.start), - tsDoc.positionAt(end.start + end.length) - ), - tsDoc.getURL(), - container - ) - ); - } - if (tree.childItems) { - for (const child of tree.childItems) { - collectSymbols(child, tree.text, cb); + function next() { + if (tree.childItems) { + for (const child of tree.childItems) { + collectSymbols(child, cb); + } } } } + + collectSymbols(navTree, (symbol) => symbols.push(symbol)); + return symbols; } async getCompletions( diff --git a/packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts b/packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts index a4257e343..ac54e95e8 100644 --- a/packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts +++ b/packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts @@ -69,9 +69,9 @@ describe('TypescriptPlugin', function () { .map((s) => ({ ...s, name: harmonizeNewLines(s.name) })) .sort( (s1, s2) => - s1.location.range.start.line * 100 + - s1.location.range.start.character - - (s2.location.range.start.line * 100 + s2.location.range.start.character) + s1.range.start.line * 100 + + s1.range.start.character - + (s2.range.start.line * 100 + s2.range.start.character) ); assert.deepStrictEqual(symbols, [ From e7c24da5d2d1c1a8ff420dfef4569593aa76354b Mon Sep 17 00:00:00 2001 From: Dimas Firmansyah Date: Fri, 8 Aug 2025 18:48:26 +0700 Subject: [PATCH 2/4] build the children tree manually --- .../language-server/src/plugins/PluginHost.ts | 64 +++++++++++++++++-- .../plugins/typescript/TypeScriptPlugin.ts | 8 +-- 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/packages/language-server/src/plugins/PluginHost.ts b/packages/language-server/src/plugins/PluginHost.ts index 04ecb6b9f..9360d6236 100644 --- a/packages/language-server/src/plugins/PluginHost.ts +++ b/packages/language-server/src/plugins/PluginHost.ts @@ -296,6 +296,30 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { ); } + private flattenSymbolTree(node: DocumentSymbol) { + const result = [node]; + for (const child of node.children || []) { + result.push(...this.flattenSymbolTree(child)); + } + node.children = []; + return result; + } + + 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 getDocumentSymbols( textDocument: TextDocumentIdentifier, cancellationToken: CancellationToken @@ -308,14 +332,40 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { if (cancellationToken.isCancellationRequested) { return []; } - return flatten( - await this.execute( - 'getDocumentSymbols', - [document, cancellationToken], - ExecuteMode.Collect, - 'high' - ) + + const results = await this.execute( + 'getDocumentSymbols', + [document, cancellationToken], + ExecuteMode.Collect, + 'high' ); + + const symbols = results + .flatMap((arr) => arr.flatMap((s) => this.flattenSymbolTree(s))) + .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( diff --git a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts index f0418c5d7..3b90f634d 100644 --- a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts +++ b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts @@ -260,11 +260,7 @@ export class TypeScriptPlugin return []; } - const hierarchy = - this.configManager.getClientCapabilities()?.textDocument?.documentSymbol - ?.hierarchicalDocumentSymbolSupport ?? false; const navTree = lang.getNavigationTree(tsDoc.filePath); - const symbols: DocumentSymbol[] = []; function collectSymbols(tree: NavigationTree, cb: (symbol: DocumentSymbol) => void) { @@ -281,7 +277,6 @@ export class TypeScriptPlugin tsDoc.positionAt(end.start + end.length) ) ); - const children: DocumentSymbol[] = []; if ( range.start.line < 0 || @@ -329,10 +324,9 @@ export class TypeScriptPlugin name = name.substring(name.indexOf('$on')); } - const symbol = DocumentSymbol.create(name, undefined, kind, range, range, children); + const symbol = DocumentSymbol.create(name, undefined, kind, range, range, undefined); cb(symbol); - if (hierarchy) cb = (s) => children.push(s); next(); function next() { From 369d835e7f614220b37f4a8bf828bb35b3236e5d Mon Sep 17 00:00:00 2001 From: Dimas Firmansyah Date: Fri, 8 Aug 2025 19:02:53 +0700 Subject: [PATCH 3/4] Revert "returns DocumentSymbol[] on textDocument/documentSymbol" This reverts commit b3dcd3aeb343953646285d06f3e6ba66ab6f10e8. --- .../src/lib/documents/DocumentMapper.ts | 10 +- .../language-server/src/plugins/PluginHost.ts | 7 +- .../src/plugins/css/CSSPlugin.ts | 24 ++-- .../src/plugins/html/HTMLPlugin.ts | 8 +- .../language-server/src/plugins/interfaces.ts | 3 +- .../plugins/typescript/TypeScriptPlugin.ts | 107 ++++++++++-------- .../typescript/TypescriptPlugin.test.ts | 6 +- 7 files changed, 90 insertions(+), 75 deletions(-) diff --git a/packages/language-server/src/lib/documents/DocumentMapper.ts b/packages/language-server/src/lib/documents/DocumentMapper.ts index 3c1c8ad43..fe79fca5a 100644 --- a/packages/language-server/src/lib/documents/DocumentMapper.ts +++ b/packages/language-server/src/lib/documents/DocumentMapper.ts @@ -12,8 +12,7 @@ import { SelectionRange, TextEdit, InsertReplaceEdit, - Location, - DocumentSymbol + Location } from 'vscode-languageserver'; import { TagInformation, offsetAt, positionAt, getLineOffsets } from './utils'; import { Logger } from '../../logger'; @@ -354,10 +353,9 @@ export function mapColorPresentationToOriginal( export function mapSymbolInformationToOriginal( fragment: Pick, - info: DocumentSymbol -): DocumentSymbol { - const range = mapRangeToOriginal(fragment, info.range); - return { ...info, range, selectionRange: range }; + info: SymbolInformation +): SymbolInformation { + return { ...info, location: mapObjWithRangeToOriginal(fragment, info.location) }; } export function mapLocationLinkToOriginal( diff --git a/packages/language-server/src/plugins/PluginHost.ts b/packages/language-server/src/plugins/PluginHost.ts index 9360d6236..4adc7358e 100644 --- a/packages/language-server/src/plugins/PluginHost.ts +++ b/packages/language-server/src/plugins/PluginHost.ts @@ -35,8 +35,7 @@ import { TextEdit, WorkspaceEdit, InlayHint, - WorkspaceSymbol, - DocumentSymbol + WorkspaceSymbol } from 'vscode-languageserver'; import { DocumentManager, getNodeIfIsInHTMLStartTag } from '../lib/documents'; import { Logger } from '../logger'; @@ -323,7 +322,7 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { async getDocumentSymbols( textDocument: TextDocumentIdentifier, cancellationToken: CancellationToken - ): Promise { + ): Promise { const document = this.getDocument(textDocument.uri); // VSCode requested document symbols twice for the outline view and the sticky scroll @@ -333,7 +332,7 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { return []; } - const results = await this.execute( + const results = await this.execute( 'getDocumentSymbols', [document, cancellationToken], ExecuteMode.Collect, diff --git a/packages/language-server/src/plugins/css/CSSPlugin.ts b/packages/language-server/src/plugins/css/CSSPlugin.ts index 5e7f3278e..f89a6285e 100644 --- a/packages/language-server/src/plugins/css/CSSPlugin.ts +++ b/packages/language-server/src/plugins/css/CSSPlugin.ts @@ -50,7 +50,7 @@ import { getIdClassCompletion } from './features/getIdClassCompletion'; import { AttributeContext, getAttributeContextAtPosition } from '../../lib/documents/parseHtml'; import { StyleAttributeDocument } from './StyleAttributeDocument'; import { getDocumentContext } from '../documentContext'; -import { DocumentSymbol, FoldingRange, FoldingRangeKind } from 'vscode-languageserver-types'; +import { FoldingRange, FoldingRangeKind } from 'vscode-languageserver-types'; import { indentBasedFoldingRangeForTag } from '../../lib/foldingRange/indentFolding'; import { wordHighlightForTag } from '../../lib/documentHighlight/wordHighlight'; import { isNotNullOrUndefined, urlToPath } from '../../utils'; @@ -362,7 +362,7 @@ export class CSSPlugin .map((colorPres) => mapColorPresentationToOriginal(cssDocument, colorPres)); } - getDocumentSymbols(document: Document): DocumentSymbol[] { + getDocumentSymbols(document: Document): SymbolInformation[] { if (!this.featureEnabled('documentColors')) { return []; } @@ -373,14 +373,20 @@ export class CSSPlugin return []; } - function mapSymbol(symbol: DocumentSymbol) { - symbol.children = symbol.children?.map(mapSymbol); - return mapSymbolInformationToOriginal(cssDocument, symbol); - } - return this.getLanguageService(extractLanguage(cssDocument)) - .findDocumentSymbols2(cssDocument, cssDocument.stylesheet) - .map(mapSymbol); + .findDocumentSymbols(cssDocument, cssDocument.stylesheet) + .map((symbol) => { + if (!symbol.containerName) { + return { + ...symbol, + // TODO: this could contain other things, e.g. style.myclass + containerName: 'style' + }; + } + + return symbol; + }) + .map((symbol) => mapSymbolInformationToOriginal(cssDocument, symbol)); } getFoldingRanges(document: Document): FoldingRange[] { diff --git a/packages/language-server/src/plugins/html/HTMLPlugin.ts b/packages/language-server/src/plugins/html/HTMLPlugin.ts index fe7a85002..2e900f626 100644 --- a/packages/language-server/src/plugins/html/HTMLPlugin.ts +++ b/packages/language-server/src/plugins/html/HTMLPlugin.ts @@ -10,6 +10,7 @@ import { CompletionList, Hover, Position, + SymbolInformation, CompletionItem, CompletionItemKind, TextEdit, @@ -18,8 +19,7 @@ import { LinkedEditingRanges, CompletionContext, FoldingRange, - DocumentHighlight, - DocumentSymbol + DocumentHighlight } from 'vscode-languageserver'; import { DocumentManager, @@ -300,7 +300,7 @@ export class HTMLPlugin return isInsideMoustacheTag(document.getText(), node.start, offset); } - getDocumentSymbols(document: Document): DocumentSymbol[] { + getDocumentSymbols(document: Document): SymbolInformation[] { if (!this.featureEnabled('documentSymbols')) { return []; } @@ -310,7 +310,7 @@ export class HTMLPlugin return []; } - return this.lang.findDocumentSymbols2(document, html); + return this.lang.findDocumentSymbols(document, html); } rename(document: Document, position: Position, newName: string): WorkspaceEdit | null { diff --git a/packages/language-server/src/plugins/interfaces.ts b/packages/language-server/src/plugins/interfaces.ts index 9614366cd..c050b2387 100644 --- a/packages/language-server/src/plugins/interfaces.ts +++ b/packages/language-server/src/plugins/interfaces.ts @@ -22,7 +22,6 @@ import { DefinitionLink, Diagnostic, DocumentHighlight, - DocumentSymbol, FoldingRange, FormattingOptions, Hover, @@ -98,7 +97,7 @@ export interface DocumentSymbolsProvider { getDocumentSymbols( document: Document, cancellationToken?: CancellationToken - ): Resolvable; + ): Resolvable; } export interface DefinitionsProvider { diff --git a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts index 3b90f634d..1b5781699 100644 --- a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts +++ b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts @@ -12,7 +12,6 @@ import { DefinitionLink, Diagnostic, DocumentHighlight, - DocumentSymbol, FileChangeType, FoldingRange, Hover, @@ -36,7 +35,6 @@ import { Document, DocumentManager, getTextInRange, - mapRangeToOriginal, mapSymbolInformationToOriginal } from '../../lib/documents'; import { LSConfigManager, LSTypescriptConfig } from '../../ls-config'; @@ -249,7 +247,7 @@ export class TypeScriptPlugin async getDocumentSymbols( document: Document, cancellationToken?: CancellationToken - ): Promise { + ): Promise { if (!this.featureEnabled('documentSymbols')) { return []; } @@ -261,85 +259,100 @@ export class TypeScriptPlugin } const navTree = lang.getNavigationTree(tsDoc.filePath); - const symbols: DocumentSymbol[] = []; - function collectSymbols(tree: NavigationTree, cb: (symbol: DocumentSymbol) => void) { - const start = tree.spans[0]; - const end = tree.spans[tree.spans.length - 1]; - if (!(start && end)) return next(); - - let name = tree.text; - const kind = symbolKindFromString(tree.kind); - const range = mapRangeToOriginal( - tsDoc, - Range.create( - tsDoc.positionAt(start.start), - tsDoc.positionAt(end.start + end.length) - ) - ); + const symbols: SymbolInformation[] = []; + collectSymbols(navTree, undefined, (symbol) => symbols.push(symbol)); + + const topContainerName = symbols[0].name; + const result: SymbolInformation[] = []; + + for (let symbol of symbols.slice(1)) { + if (symbol.containerName === topContainerName) { + symbol.containerName = 'script'; + } + + symbol = mapSymbolInformationToOriginal(tsDoc, symbol); if ( - range.start.line < 0 || - range.end.line < 0 || - isZeroLengthRange(range) || - name.startsWith('__sveltets_') + symbol.location.range.start.line < 0 || + symbol.location.range.end.line < 0 || + isZeroLengthRange(symbol.location.range) || + symbol.name.startsWith('__sveltets_') ) { - return next(); + continue; } if ( - (kind === SymbolKind.Property || kind === SymbolKind.Method) && - !isInScript(range.start, document) + (symbol.kind === SymbolKind.Property || symbol.kind === SymbolKind.Method) && + !isInScript(symbol.location.range.start, document) ) { if ( - name === 'props' && - document.getText().charAt(document.offsetAt(range.start)) !== 'p' + symbol.name === 'props' && + document.getText().charAt(document.offsetAt(symbol.location.range.start)) !== + 'p' ) { // This is the "props" of a generated component constructor - return next(); + continue; } - const node = tsDoc.svelteNodeAt(range.start); + const node = tsDoc.svelteNodeAt(symbol.location.range.start); if ( (node && (isAttributeName(node) || isAttributeShorthand(node))) || isEventHandler(node) ) { // This is a html or component property, they are not treated as a new symbol // in JSX and so we do the same for the new transformation. - return next(); + continue; } } - if (name === '') { - name = getTextInRange(range, document.getText()).trimStart(); + if (symbol.name === '') { + let name = getTextInRange(symbol.location.range, document.getText()).trimLeft(); if (name.length > 50) { name = name.substring(0, 50) + '...'; } + symbol.name = name; } - if (name.startsWith('$$_')) { - if (!name.includes('$on')) { - return next(); + if (symbol.name.startsWith('$$_')) { + if (!symbol.name.includes('$on')) { + continue; } // on:foo={() => ''} -> $on("foo") callback - name = name.substring(name.indexOf('$on')); + symbol.name = symbol.name.substring(symbol.name.indexOf('$on')); } - const symbol = DocumentSymbol.create(name, undefined, kind, range, range, undefined); + result.push(symbol); + } - cb(symbol); - next(); + return result; - function next() { - if (tree.childItems) { - for (const child of tree.childItems) { - collectSymbols(child, cb); - } + function collectSymbols( + tree: NavigationTree, + container: string | undefined, + cb: (symbol: SymbolInformation) => void + ) { + const start = tree.spans[0]; + const end = tree.spans[tree.spans.length - 1]; + if (start && end) { + cb( + SymbolInformation.create( + tree.text, + symbolKindFromString(tree.kind), + Range.create( + tsDoc.positionAt(start.start), + tsDoc.positionAt(end.start + end.length) + ), + tsDoc.getURL(), + container + ) + ); + } + if (tree.childItems) { + for (const child of tree.childItems) { + collectSymbols(child, tree.text, cb); } } } - - collectSymbols(navTree, (symbol) => symbols.push(symbol)); - return symbols; } async getCompletions( diff --git a/packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts b/packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts index ac54e95e8..a4257e343 100644 --- a/packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts +++ b/packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts @@ -69,9 +69,9 @@ describe('TypescriptPlugin', function () { .map((s) => ({ ...s, name: harmonizeNewLines(s.name) })) .sort( (s1, s2) => - s1.range.start.line * 100 + - s1.range.start.character - - (s2.range.start.line * 100 + s2.range.start.character) + s1.location.range.start.line * 100 + + s1.location.range.start.character - + (s2.location.range.start.line * 100 + s2.location.range.start.character) ); assert.deepStrictEqual(symbols, [ From 83430d8fb594dd7247969ab3a240366f786389f4 Mon Sep 17 00:00:00 2001 From: Dimas Firmansyah Date: Fri, 8 Aug 2025 19:18:04 +0700 Subject: [PATCH 4/4] map from flat SymbolInformation[] to DocumentSymbol tree --- .../language-server/src/plugins/PluginHost.ts | 63 +++++++++++-------- packages/language-server/src/server.ts | 13 +++- 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/packages/language-server/src/plugins/PluginHost.ts b/packages/language-server/src/plugins/PluginHost.ts index 4adc7358e..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'; @@ -295,13 +296,27 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { ); } - private flattenSymbolTree(node: DocumentSymbol) { - const result = [node]; - for (const child of node.children || []) { - result.push(...this.flattenSymbolTree(child)); + async getDocumentSymbols( + textDocument: TextDocumentIdentifier, + cancellationToken: CancellationToken + ): Promise { + const document = this.getDocument(textDocument.uri); + + // VSCode requested document symbols twice for the outline view and the sticky scroll + // Manually delay here and don't use low priority as one of them will return no symbols + await new Promise((resolve) => setTimeout(resolve, 1000)); + if (cancellationToken.isCancellationRequested) { + return []; } - node.children = []; - return result; + + return flatten( + await this.execute( + 'getDocumentSymbols', + [document, cancellationToken], + ExecuteMode.Collect, + 'high' + ) + ); } private comparePosition(pos1: Position, pos2: Position) { @@ -319,28 +334,22 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { ); } - async getDocumentSymbols( + async getHierarchicalDocumentSymbols( textDocument: TextDocumentIdentifier, cancellationToken: CancellationToken - ): Promise { - const document = this.getDocument(textDocument.uri); - - // VSCode requested document symbols twice for the outline view and the sticky scroll - // Manually delay here and don't use low priority as one of them will return no symbols - await new Promise((resolve) => setTimeout(resolve, 1000)); - if (cancellationToken.isCancellationRequested) { - return []; - } - - const results = await this.execute( - 'getDocumentSymbols', - [document, cancellationToken], - ExecuteMode.Collect, - 'high' - ); - - const symbols = results - .flatMap((arr) => arr.flatMap((s) => this.flattenSymbolTree(s))) + ): 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; 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)