public static List<Either<SymbolInformation, DocumentSymbol>> documentSymbols(EventDocumentContext context) {
        // providing scope = null yields all symbols
        List<Symbol> allSymbols = context.schemaIndex.listSymbolsInScope(null, EnumSet.allOf(SymbolType.class));

        // three stages: first find relevant symbols and construct LSP "DocumentSymbol" objects from them.
        //               second go through and create the DocumentSymbol tree structure by adding a symbol to the children list of its scope.
        //               last, only add the roots of this forest to the result
        Map<Symbol, DocumentSymbol> symbols = new HashMap<>();
        for (Symbol symbol : allSymbols) {
            if (!symbol.fileURIEquals(context.document.getFileURI())) continue;

            // this case can happen if the schema is not correct, so some identifiers become empty
            // It will not cause a server side error, but the client crashes (at least vscode)
            if (symbol.getShortIdentifier() == null || symbol.getShortIdentifier().isBlank()) continue;

            symbols.put(symbol, new DocumentSymbol(
                symbol.getShortIdentifier(), 
                schemaSymbolTypeToLSPSymbolKind(symbol.getType()), 
                getSymbolRange(symbol),
                // selection range is the small range at the identifier
                symbol.getNode().getRange(), 
                symbol.getPrettyIdentifier(), 
                new ArrayList<>()));
        }

        for (var entry : symbols.entrySet()) {
            Symbol child = entry.getKey();
            Symbol parent = child.getScope();
            if (parent != null && symbols.containsKey(parent)) {
                symbols.get(parent).getChildren().add(entry.getValue());
            }
        }

        // Finally we add only the roots to the result.
        List<Either<SymbolInformation, DocumentSymbol>> result = new ArrayList<>();
        for (var entry : symbols.entrySet()) {
            if (entry.getKey().getScope() == null || !symbols.containsKey(entry.getKey().getScope())) {
                result.add(Either.forRight(entry.getValue()));
            }
        }
        return result;
    }
The range of a symbol should enclose the entirety of its definition including a body. 
This allows UI to display the hierarchy at the cursor position.
integration/schema-language-server/language-server/src/main/java/ai/vespa/schemals/lsp/schema/documentsymbols/SchemaDocumentSymbols.java